<div class="wrapper">
  <div class="wrapper__inner">
    <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 {
          grid-column: 2;
          font-weight: 500;
          font-size: 24px;
          line-height: 36px;
          outline: none;
          letter-spacing: -0.0125em;
        }
      </style>
      <g>
        <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>
      </g>
    </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>
    <p class="controls__saving-disabled">Saving is disabled on mobile devices</p>
  </div>

</div>
* {
  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;
  }

  .controls__btn--save {
    display: none;
  }

  .controls__saving-disabled {
    width: 100%;
    display: block;
  }
}
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";

console.clear();

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");

let baseColor;
let baseColorWhite;
let baseColorBlack;

let complimentaryColor1;
let complimentaryColor2;

let shapeColors;

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

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

setColors();
generate();

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

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

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);
  }
}

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

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));
}

function randomColor() {
  return shapeColors[~~random(0, shapeColors.length)];
}

function randomAlignment() {
  return alignmentOpts[~~random(0, alignmentOpts.length)];
}

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 random(min, max) {
  return Math.random() * (max - min) + min;
}

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
  );
}

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

// 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(".wrapper__inner"), {
    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");
    });
  });
});
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.