<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");
});
});
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.