Create generative social images using SVG!
Tags: tutorial, webdev, javascript, design
Published: 2021-01-25 16:39:39

So... it's 2021. The web is an endless sea of beautiful, weird, terrifying stuff. How do you make sure your site cuts through the noise? 

Well, alongside some great content, I think an awesome **generative** social image _(just like the one used for this tutorial!)_ could be a good step along the way ✨

Let’s make some!

---

# The end result

First things first, let’s skip to the end. Here is the final result of this tutorial: 

{% codepen https://codepen.io/georgedoescode/pen/PoGLmmX %}

It's a scalable, editable, self-generating social image! If you click on the buttons in the CodePen above or change the text content, you should see the image magically re-design itself 🔮

---

# But... what's it actually for? 

A *"social image"* as I call them, or *"meta image"*, is the little preview that shows up in Slack / Twitter / Facebook whenever you paste a link.

Here are some social image examples found in the wild...

An awesome textured design from [Stephanie Eckles](https://twitter.com/5t3ph):

![A custom, textured social image](https://dev-to-uploads.s3.amazonaws.com/i/snzbkyx9h95ynlxwr0ig.png)

The much-loved social image from [DEV](https://dev.to/) + [Ben Halpern](https://twitter.com/bendhalpern): 

![DEV community social image](https://dev-to-uploads.s3.amazonaws.com/i/ae44nmwc93g73x7539b6.png)

Some very cool 3D vibes from [Josh Comeau](https://twitter.com/JoshWComeau): 

![A custom 3d style social image on Twitter](https://dev-to-uploads.s3.amazonaws.com/i/l7y5oaws07oinhpuuyma.png)

Although all of my examples are from Twitter, it's important to remember _(and a huge benefit to creating your social images with SVG)_ that different sites **can** require different dimensions. 

Luckily, through the power of `SVG` + `viewBox`, the images we are going to create in this tutorial can be simply resized to any dimensions/aspect ratio. Nice!

---

# Blast off 🚀

OK, I think that's enough preamble. We are ready to start building. Overalls on, folks!

## HTML Markup

First things first let's add some HTML for our page: 

```html
<div class="wrapper">
  <div class="social-image-wrapper">
  <!-- Save a space for our SVG! -->
  </div>
  <div class="controls">
    <div class="controls__randomize">
      <p class="controls__label">Randomize:</p>
      <button class="controls__btn controls__btn--alignment">Alignment</button>
      <button class="controls__btn controls__btn--colors">Colors</button>
      <button class="controls__btn controls__btn--shapes">Shapes</button>
    </div>
    <button class="controls__btn controls__btn--save">Save</button>
  </div>
</div>
```
 
In this code snippet, we are adding the HTML markup we need for our UI and popping everything inside a nice little wrapper `div`.

---

## SVG Markup

Once we have added the HTML for the user interface it's for the main markup event. I mentioned earlier that our social images are going to be created using the `<svg>` element, so let's add one to our `social-image-wrapper`:

```html
<div class="social-image-wrapper">
  <svg
    viewBox="0 0 1200 630"
    xmlns="http://www.w3.org/2000/svg"
    class="social-image"
  >
    <foreignObject x="0" y="0" width="1200" height="630">
      <div class="social-image__html">
        <div class="social-image__text">
          <h1
            xmlns="http://www.w3.org/1999/xhtml"
            class="social-image__title"
            contenteditable
          >
            All of this text is editable... click on it and start typing!
          </h1>
          <h2
            xmlns="http://www.w3.org/1999/xhtml"
            class="social-image__meta"
            contenteditable
          >
            As you type, the background will adapt itself to the text, making
            sure the shapes never overlap.
          </h2>
        </div>
      </div>
    </foreignObject>
  </svg>
</div>
```

There is quite a bit to unpack here but don't worry! We can work through it together 🤝

### viewBox

First off, we are creating our `<svg>` element and defining a [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox):

```html
<svg
  viewBox="0 0 1200 630"
  xmlns="http://www.w3.org/2000/svg"
  class="social-image"
>
  ...
</svg>
```

The `viewBox` attribute defines the coordinate space in which all of the contents of our `<svg>` will be drawn. In our case, this is `1200x630px`. 

Through the power of `viewBox`, we can position/scale everything relative to a fixed coordinate space, whilst the `<svg>` itself will be able to scale to any size. Powerful stuff ⚡

### foreignObject

Next, we add a `foreignObject` tag filled with some HTML to our `<svg>` element:

```html
<foreignObject x="0" y="0" width="1200" height="630">
  ...
</foreignObject>
```

This is where things start getting interesting! [foreignObject](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject) can be used to add content from another XML namespace (in our case, HTML) to an `<svg>` element. 

Once added, this `HTML` will automatically scale to the `viewBox` just like regular `SVG` content. This is incredibly powerful, as it allows us to style the contents of our social image using CSS whilst retaining the fluidity and rendering power of SVG. 

More on that shortly. 

__Note: any `HTML` elements added within `foreignObject` must posses an `xmlns="http://www.w3.org/1999/xhtml"` attribute.__

### The contenteditable attribute

The last thing to check out here is the `contenteditable` attribute added to our `h1` and `h2` tags: 

```html
<h1
  xmlns="http://www.w3.org/1999/xhtml"
  class="social-image__title"
  contenteditable
>
  All of this text is editable... click on it and start typing!
</h1>
```

[contenteditable](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) simply allows the user to edit the text within `HTML` tags. This is perfect for us, as it means users will easily be able to add their own content and instantly preview the result.

---

## Style time 💅 

OK, so, we now have all the markup we need to create beautiful social images. Things are probably looking a little bit sad though. We should really fix that. 

### Page styles

First up, let's add some styles for our UI:

```css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --black: hsl(0, 0%, 10%);
}

body {
  width: 100vw;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 2rem;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  color: var(--black);
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.wrapper {
  width: 100%;
  max-width: 60rem;
  min-width: 20rem;
  margin: 0 auto;
  overflow: hidden;
}

.controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  margin: 2rem 0;
}

.controls__label {
  margin-right: 1rem;
  font-weight: 500;
  font-size: 1rem;
}

.controls__randomize {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  flex-wrap: wrap;
}

.controls__btn {
  width: 8rem;
  height: 2.25rem;
  margin-right: 1rem;
  background: #fff;
  border-radius: 0;
  border: none;
  border: 2px solid var(--black);
  font-family: inherit;
  color: var(--black);
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
}

.controls__btn:hover {
  background: var(--black);
  color: #fff;
}

.controls__btn--save {
  position: relative;
  margin-left: auto;
  margin-right: 0;
  background: var(--black);
  color: #fff;
}

.controls__btn--save:hover {
  background: #fff;
  color: var(--black);
}

.controls__saving-disabled {
  font-size: 0.875rem;
  margin-top: 2rem;
  font-weight: 500;
  display: none;
  font-style: italic;
}

@media only screen and (max-width: 800px) {
  body {
    padding: 0.75rem;
  }

  .controls__btn {
    width: 6rem;
    height: 2rem;
    font-size: 0.875rem;
    margin-top: 0.75rem;
  }

  .controls__label {
    font-size: 0.875rem;
    margin-right: 0.5rem;
    width: 100%;
  }
  .controls__btn--save {
    width: 100%;
    margin-top: 1.25rem;
  }
}

@media only screen and (max-width: 480px) {
  .controls__btn {
    margin-right: 0.5rem;
  }
}
```

I won't go too deep on this CSS, as it's not the main feature here. If you do have any questions about these styles, though, do feel free to drop me a message.

### Social image styles

Next, let's add an internal `<style>` tag to our `<svg>` element. This will contain all the styles for the social image itself:

```html
<svg
  viewBox="0 0 1200 630"
  xmlns="http://www.w3.org/2000/svg"
  class="social-image"
>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .social-image {
      --align-text-x: flex-start;
      --align-text-y: flex-end;

      width: 100%;
      background: #f5f7fa;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
        "Segoe UI Symbol";
      line-height: 1;
    }

    .social-image__html {
      display: flex;
      height: 100%;
      justify-content: var(--align-text-x);
      align-items: var(--align-text-y);
      padding: 72px;
    }

    .social-image__text {
      max-width: 700px;
    }

    .social-image__title {
      font-size: 56px;
      line-height: 68px;
      font-weight: 800;
      margin-bottom: 24px;
      letter-spacing: -0.0125em;
      outline: none;
    }

    .social-image__meta {
      font-weight: 500;
      font-size: 24px;
      line-height: 36px;
      outline: none;
      letter-spacing: -0.0125em;
    }
  </style>
  ...
</svg>
```

We are adding this CSS to an internal `<style>` tag as I had some issues with `html2canvas` not rendering as expected with the styles living outside of the `<svg>`. It's also nice to keep things contained.

Again, I won't go into too much detail with the CSS here, but the key effects of this stylesheet are: 

- Set up some [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) to handle the positioning of our text within the social image, in combination with [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/flex). We can modify these custom properties later using JavaScript. 

- Add some typographic style to the text content. We are using *system fonts* here. It is possible to use custom fonts, but doing so adds a little complexity as the fonts need to be embedded within the `<svg>`. Maybe next time!

### Our progress so far
 
Now that both of these stylesheets have been added in their respective positions, you should hopefully be seeing something like this in your browser: 

![A simple social image design being resized](https://dev-to-uploads.s3.amazonaws.com/i/fpfnldjdn4qcaxo3xh3o.gif)

Pretty cool eh! As you resize your browser, check out how our HTML magically scales along with our `<svg>` element ✨

At this point, we are all set up and ready to make things beautiful. Let's head over to JS-town and make that happen 🎨

---

## Next stop, JavaScript central 🚂

### Package installation

Let's get the boring stuff out of the way first and install the packages we need for this project. The packages we will be using are: 

- [svg.js](https://www.npmjs.com/package/svgjs) - Used to simplify SVG scripting (creating and updating SVG elements such as `<circle>`)
- [html2canvas](https://www.npmjs.com/package/html2canvas) - Used to take a screenshot of our `<svg>` social image so that it can be downloaded
- [file-saver](https://www.npmjs.com/package/file-saver) - Used to handle the saving of our social image once it has been captured by `html2canvas`
- [resize-observer-polyfill](https://www.npmjs.com/package/resize-observer-polyfill) - Adds a polyfill for `ResizeObserver` to browsers that do not support it

If you are following along on CodePen, you can simply add these imports to your JS file: 

```javascript
import { SVG } from "https://cdn.skypack.dev/@svgdotjs/svg.js";
import html2canvas from "https://cdn.skypack.dev/html2canvas@1.0.0-rc.7";
import ResizeObserver from "https://cdn.skypack.dev/resize-observer-polyfill@1.5.1";
import FileSaver from "https://cdn.skypack.dev/file-saver@2.0.5";
```

If you are working in your own environment, you can install the packages you need with: 

```bash
npm i svgjs html2canvas resize-observer-polyfill file-saver 
```

The packages can then be imported like so: 

```javascript
import { SVG } from "svg.js";
import html2canvas from "html2canvas";
import ResizeObserver from "resize-observer-polyfill";
import FileSaver from "file-saver";
```

**Note: If you are working in your own environment, you will need a bundler such as Webpack or Parcel to handle these imports.**

### DOM Element references 

Now that we have all the packages we need for this project, we should add some variables that reference our various DOM elements **(buttons, the social image svg, etc)** 

To do so we can add: 

```javascript
const socialImageSVG = document.querySelector(".social-image");
const socialImageTitle = document.querySelector(".social-image__title");
const socialImageMeta = document.querySelector(".social-image__meta");

const saveBtn = document.querySelector(".controls__btn--save");
const alignmentBtn = document.querySelector(".controls__btn--alignment");
const colorBtn = document.querySelector(".controls__btn--colors");
const shapesBtn = document.querySelector(".controls__btn--shapes");
```

### Colors 

Next up on the list is defining some color variables. These will store a bunch of HSL colors that we will define a little later and eventually use to color our social image: 

```javascript
let baseColor;
let baseColorWhite;
let baseColorBlack;

let complimentaryColor1;
let complimentaryColor2;

let shapeColors;
```

Nice. All our colors are empty right now, but that's fine.

### Alignment options 

In addition to random colors, our social image will also allow random alignment of its text. To facilitate this a little further down the track, let's store the `flex` properties we want to use to control alignment in an array: 

```javascript
const alignmentOpts = ["flex-start", "flex-end", "center"];
```

Lovely. We will use these values shortly. 

### Set up an svg.js instance 

We are going to use [svg.js](https://svgjs.com/docs/3.0/) here to allow for quick, easy SVG scripting. Without svg.js, creating and updating SVG elements can get **very** wordy.

We can create a new svg.js instance like so: 

```javascript
const shapes = SVG(socialImageSVG).group();
```

What this line is saying is - Create me a new SVG `<group>` element, inside our root `<svg>` that I can easily draw into with methods such as `shapes.rect(...)`.

### Adding the `random()` utility function 

Before we go any further, lets quickly add a small utility function `random`, which generates a random number within a range: 

```javascript
function random(min, max) {
  return Math.random() * (max - min) + min;
}
```

This is a super handy utility. Definitely save it for later if you fancy trying your hand at some more generative stuff! I use it all the time. 

### Choose some random colors 

Often in my tutorials, I hold the colors until right at the end, but I think for this one we should define them early. They are such an integral part of the end result and having them set will make following the code in the upcoming steps a  bit easier. 

To generate some random colors, we can add the following `setColors` function:

```javascript
function setColors() {
  const baseHue = random(0, 360);
  const saturation = random(60, 90);

  baseColor = `hsl(${baseHue}, ${saturation}%, 60%)`;
  baseColorWhite = `hsl(${baseHue}, ${saturation}%, 97%)`;
  baseColorBlack = `hsl(${baseHue}, 95%, 3%)`;

  complimentaryColor1 = `hsl(${baseHue + 90}, ${saturation}%, 60%)`;
  complimentaryColor2 = `hsl(${baseHue + 180}, ${saturation}%, 60%)`;

  shapeColors = [complimentaryColor1, complimentaryColor2, baseColor];

  socialImageSVG.style.background = baseColorWhite;
  socialImageSVG.style.color = baseColorBlack;
}
```

Here's what this function is doing: 

1. Pick a random hue, somewhere between 0 and 360
2. Pick a random saturation, somewhere between 60 and 90
3. Define a base color, a very dark color, and a very light color all based on the same hue. This is a great way to create simple color palettes and keep things consistent 
4. Choose two complementary colors, each with a hue 90 degrees away from the previous, with the same saturation and lightness. This is another great, simple way of finding colors that work together
5. Store the complimentary and base colors in our `shapeColors` array. We will use these later to fill in our shapes 
6. Set the background of our social image to the very light color, and it's text color to the very dark color

Now if we call `setColors()`, we should see the background and text colors of our social image change. It will be very subtle. Hopefully, something like this: 

![A colorful social image](https://dev-to-uploads.s3.amazonaws.com/i/pz3wn8cp7lc1diqwfysg.png)

Looking good. Onwards!

### Creating random shape positions 

Next on our list is to generate some random, non-overlapping rectangles to position our shapes within. We want these rectangles to not only avoid overlapping each other but also avoid overlapping our text.

#### A small problem

To avoid overlapping our text when creating random rectangles, we need to know each text element's dimensions **relative** to our `<svg>`'s `viewBox`. 

Often for this purpose we would use [getBBox](https://developer.mozilla.org/en-US/docs/Web/API/SVGGraphicsElement/getBBox) however `getBBox` is only available for SVG elements, and our text is HTML. 

This isn't so bad, we can create our own `relativeBounds` function that will solve this for us in no time!

Here it is: 

```javascript
function relativeBounds(svg, HTMLElement) {
  const { x, y, width, height } = HTMLElement.getBoundingClientRect();

  const startPoint = svg.createSVGPoint();
  startPoint.x = x;
  startPoint.y = y;

  const endPoint = svg.createSVGPoint();
  endPoint.x = x + width;
  endPoint.y = y + height;

  const startPointTransformed = startPoint.matrixTransform(
    svg.getScreenCTM().inverse()
  );
  const endPointTransformed = endPoint.matrixTransform(
    svg.getScreenCTM().inverse()
  );

  return {
    x: startPointTransformed.x,
    y: startPointTransformed.y,
    width: endPointTransformed.x - startPointTransformed.x,
    height: endPointTransformed.y - startPointTransformed.y
  };
}
```

Cool! I won't go too deep on this function as I appreciate it is rather dry, but it essentially gives us `getBBox` functionality for HTML elements within an SVG.

Now that we have our `relativeBounds` function, we can generate our shape positions. 

Lets add a `generateRandomRects` and a `detectRectCollision` function: 

```javascript
function generateRandomRects(existing) {
  const rects = [...existing];
  const tries = 250;
  const maxShapes = 6;

  for (let i = 0; i < tries; i++) {
    if (rects.length === maxShapes + existing.length) break;

    const size = random(100, 600);

    const rect = {
      x: random(-size, 1200),
      y: random(-size, 630),
      width: size,
      height: size
    };

    if (!rects.some((r) => detectRectCollision(r, rect))) {
      rects.push(rect);
    }
  }

  return rects;
}

function detectRectCollision(rect1, rect2, padding = 32) {
  return (
    rect1.x < rect2.x + rect2.width + padding &&
    rect1.x + rect1.width + padding > rect2.x &&
    rect1.y < rect2.y + rect2.height + padding &&
    rect1.y + rect1.height + padding > rect2.y
  );
}
```

To break this down: 

1. Store some existing rectangles in an array (in our case, the surrounding rectangles, or bounds, of our text elements) 
2. For a certain amount of tries: create a randomly sized rectangle. If this new rectangle does not overlap with any of the other rectangles, store it.
3. Once all of the tries are used up, or the maximum number of shapes reached, return the random rectangles that we managed to generate

You may notice a funny looking `padding` option in our rectangle collision code. This defines the minimum distance between rectangles. I found it helped make things look a little neater. 

#### A note on imperfection

This is far from a perfect function. It is rather slow as a result of using brute force to place our rectangles, and there is no guarantee `maxShapes` will be reached with our number of tries. 

Does that mean it's bad, though? No way. 

We are worried more about visual results than algorithmic efficiency right now, and these values seem to produce pretty _aesthetic_ looking results. **The real challenge of generative design lies the tweaking of values like this.**

You should experiment with changing these parameters. Try changing the maximum number of shapes, maybe tweak the size of our the or increase the maximum number of tries. Check out the results. Repeat. There are no right answers here! 

### Drawing our shapes 

Alright, so we have some code ready to generate the non-overlapping rectangles. Let's bring them to life! 

First, let's add a new `generate` function: 

```javascript
function generate() {
  shapes.clear();

  const htmlRects = [
    relativeBounds(socialImageSVG, socialImageTitle),
    relativeBounds(socialImageSVG, socialImageMeta)
  ];

  const rects = generateRandomRects(htmlRects);

  for (const rect of rects.slice(2, rects.length)) {
    drawRandomShape(rect);
  }
}
```

This is actually quite a small block of code. `generateRandomRects` is doing most of the heavy lifting here. We are saying: 

1. Clear out any shapes that already exist (this will be useful later when dynamically re-generating the image)
2. Store the bounds of our two text elements, relative to the `viewBox`, in an array
3. Generate a bunch of random, non-overlapping rectangles
4. For every random rectangle (apart from the first two text rectangles) draw a random shape **within** it.

Now, we don't actually have a `drawRandomShape` function right now. Let's add one. As a simple start, try this: 

```javascript
function drawRandomShape(rect) {
  const { x, y, width, height } = rect;
  shapes.rect(width, height).x(x).y(y);
}
```

Once you have added `drawRandomShape`, you can safely call `generate` without your browser getting mad at you: 

```javascript
generate();
```

If you check out the browser now, you should see something like this: 

![Social image with random rectangles](https://dev-to-uploads.s3.amazonaws.com/i/vnock9439moo7n3l8jdt.png)

Pretty nice! These are the random rectangles we generated earlier, presented in a very simple way. 

We can expand, though. Let's update`drawRandomShape` and add a small `randomColor` utility function: 

```javascript
function randomColor() {
  // ~~ === shorthand for Math.floor()
  return shapeColors[~~random(0, shapeColors.length)];
}

function drawRandomShape({ x, y, width, height }) {
  const shapeChoices = ["rect", "ellipse", "triangle"];
  let shape;

  switch (shapeChoices[~~random(0, shapeChoices.length)]) {
    case "ellipse":
      shape = shapes.ellipse(width, height).x(x).y(y);
      break;
    case "triangle":
      shape = shapes
        .polygon(`0 ${height}, ${width / 2} 0, ${width} ${height}`)
        .x(x)
        .y(y);
      break;
    default:
      shape = shapes.rect(width, height).x(x).y(y);
  }

  const color = randomColor();

  if (random(0, 1) > 0.25) {
    shape.fill(color);
  } else {
    shape
      .stroke({
        color,
        width: 16
      })
      .fill("transparent");
  }

  shape.node.classList.add("shape");
  shape.rotate(random(0, 90)).scale(0.825);
  shape.opacity(random(0.5, 1));
}
```

Here's a breakdown of what's happening here: 

1. Pick a random shape type 
2. Use svg.js to render a different SVG element based on our shape choice 
3. Pick a random color from the choices we defined earlier 
4. 25% of the time, apply this color to the shape outline. The other 75% fill the shape with this color
5. Add a class of `shape` to the element so that we can quickly reference it later 
5. Rotate the shape by some random value and reduce its opacity by a random amount

Phew! Things are getting pretty intense. Let's have a break and marvel at our wonderful generative creation!

![Social image with shapes and colors added](https://dev-to-uploads.s3.amazonaws.com/i/1zr0g9l0tbnj8bip317e.png)

Woah! 🤩 Looking good folks. We are pretty much there. As you refresh your browser you should see something different each time.

### Interactivity 

The last step in this tutorial is to make things interactive. This is mostly going to involve attaching event listeners to stuff and running functionality that we have already defined. 

To keep things brief, I have commented this code inline. If you do require any more detail or have any questions on this stuff just let me know! 

#### Hooking up the buttons

```javascript
// regenerate our shapes and shape positions
shapesBtn.addEventListener("click", () => {
  generate();
});

// set new random color values and update the existing shapes with these colors
colorBtn.addEventListener("click", () => {
  setColors();

  // find all the shapes in our svg and update their fill / stroke
  socialImageSVG.querySelectorAll(".shape").forEach((node) => {
    if (node.getAttribute("stroke")) {
      node.setAttribute("stroke", randomColor());
    } else {
      node.setAttribute("fill", randomColor());
    }
  });
});

// choose random new alignment options and update the CSS custom properties, regenerate the shapes
alignmentBtn.addEventListener("click", () => {
  socialImageSVG.style.setProperty("--align-text-x", alignmentOpts[~~random(0, alignmentOpts.length)]);
  socialImageSVG.style.setProperty("--align-text-y", alignmentOpts[~~random(0, alignmentOpts.length)]);
  generate();
});

// save our social image as a .png file
saveBtn.addEventListener("click", () => {
  const bounds = socialImageSVG.getBoundingClientRect();

  // on save, update the dimensions of our social image so that it exports as expected
  socialImageSVG.style.width = "1200px";
  socialImageSVG.style.height = "630px";
  socialImageSVG.setAttribute("width", 1200);
  socialImageSVG.setAttribute("height", 630);
  // this fixes an odd visual "cut off" bug when exporting
  window.scrollTo(0, 0);

  html2canvas(document.querySelector(".social-image-wrapper"), {
    width: 1200,
    height: 630,
    scale: 2 // export our image at 2x resolution so it is nice and crisp on retina devices
  }).then((canvas) => {
    canvas.toBlob(function (blob) {
      // restore the social image styles
      socialImageSVG.style.width = "100%";
      socialImageSVG.style.height = "auto";
      socialImageSVG.setAttribute("width", "");
      socialImageSVG.setAttribute("height", "");

      FileSaver.saveAs(blob, "generative-social-image.png");
    });
  });
});
```

#### Handling new text input 

Ok, so all our buttons are hooked up and that's great. There is one last feature to add though. As the user types, we want to update our shape positions. To do this we can use `ResizeObserver` to run a function each time the width/height dimensions of our text elements change. 

Check it out: 

```javascript
const resizeObserver = new ResizeObserver(() => {
  generate();
});

resizeObserver.observe(socialImageTitle);
resizeObserver.observe(socialImageMeta);
```

Now as you type you should see your social image update just like the CodePen example. 

---

## We made it! 

Blimey, that was quite something! The good news is, we are all done. I hope you learned something about generative design here and maybe even picked up some handy little SVG tips. 

I think there are lots of places you could take this and would love to hear from you if you create something cool based on this tutorial 😎

**If you did enjoy this post, do follow me on Twitter [@georgedoescode](https://twitter.com/georgedoescode) for a steady stream of creative coding fun.**

**You can also support my tutorials by [buying me a coffee ☕](https://ko-fi.com/georgedoescode)**

Thank you very much for reading! I'll catch you next time ❤️