* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--window-height: 100vh;
--shadow-offset-x: 48px;
--shadow-offset-y: 48px;
}
body {
display: grid;
grid-gap: 24px;
padding: 24px;
grid-template-columns: repeat(3, 1fr);
place-items: center;
min-height: 100vh;
background: #fff;
}
@media only screen and (max-width: 640px) {
body {
grid-template-columns: repeat(2, 1fr);
grid-gap: 16px;
padding: 16px;
}
}
.poster {
width: 100%;
background: #fff;
border: 1px solid hsla(0, 0%, 96%);
border-radius: 1rem;
}
.poster__text div {
width: 100%;
height: 100%;
display: grid;
place-content: end;
padding: 64px;
user-select: none;
font-family: "DM Sans";
hyphens: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.poster__text div h1 {
line-height: 32px;
font-size: 32px;
margin-bottom: 32px;
font-weight: 700;
text-indent: -0.5em;
}
.poster__text div p {
line-height: 24px;
font-size: 18px;
padding-right: 128px;
font-weight: 400;
}
import { Vector2D } from "https://cdn.skypack.dev/@georgedoescode/vector2d@1.0.7";
import arcToBezier from "https://cdn.skypack.dev/svg-arc-to-cubic-bezier@3.2.0";
import { SVG } from "https://cdn.skypack.dev/@svgdotjs/svg.js@3.1.2";
import { Bezier } from "https://cdn.skypack.dev/bezier-js@6.1.0";
import {
random,
seedPRNG
} from "https://cdn.skypack.dev/@georgedoescode/generative-utils@1.0.38";
// Thanks Steve Ruiz for these little helpers! https://github.com/steveruizok/globs
function shortAngleDist(a0, a1) {
const max = Math.PI * 2;
const da = (a1 - a0) % max;
return ((2 * da) % max) - da;
}
function angleDelta(a0, a1) {
return shortAngleDist(a0, a1);
}
function getSweep(c, a, b) {
return angleDelta(Vector2D.angle(c, a), Vector2D.angle(c, b));
}
// You should totally check out the original paper on globs https://jcgt.org/published/0004/03/01/paper.pdf
class Glob {
constructor(opts) {
if (!opts.start || !opts.end) {
console.warn("Warning: Glob must have a start and end node");
return;
}
if (!opts.d) {
opts.d = Vector2D.lerp(
opts.start.point.copy(),
opts.end.point.copy(),
0.5
);
} else {
opts.d = new Vector2D(opts.d.x, opts.d.y);
}
if (!opts.dP) {
opts.dP = Vector2D.lerp(
opts.start.point.copy(),
opts.end.point.copy(),
0.5
);
} else {
opts.dP = new Vector2D(opts.dP.x, opts.dP.y);
}
const defaults = {
a: 0.5,
b: 0.5,
aP: 0.5,
bP: 0.5
};
const { start, end, d, dP, a, b, aP, bP } = Object.assign(defaults, opts);
this.start = start;
this.end = end;
this.isValid = true;
this._SIDE_RIGHT = 1;
this._SIDE_LEFT = -1;
this._parameters = {};
this._geometry = {};
this.setParameters(this.start, this.end, d, dP, a, b, aP, bP);
this.buildGeometry();
}
// Convert whole shape to bezier curves for easy collision detection/offsetting etc
get bezierCurves() {
const arc1 = arcToBezier({
px: this._geometry.e0.x,
py: this._geometry.e0.y,
cx: this._geometry.e0P.x,
cy: this._geometry.e0P.y,
rx: this._parameters.r0,
ry: this._parameters.r0,
xAxisRotation: 0,
largeArcFlag:
getSweep(this._parameters.c0, this._geometry.e0, this._geometry.e0P) > 0
? 0
: 1,
sweepFlag: 1
});
const arc2 = {
x1: this._geometry.f0P.x,
y1: this._geometry.f0P.y,
x2: this._geometry.f1P.x,
y2: this._geometry.f1P.y,
x: this._geometry.e1P.x,
y: this._geometry.e1P.y
};
const arc3 = arcToBezier({
px: this._geometry.e1P.x,
py: this._geometry.e1P.y,
cx: this._geometry.e1.x,
cy: this._geometry.e1.y,
rx: this._parameters.r1,
ry: this._parameters.r1,
xAxisRotation: 0,
largeArcFlag:
getSweep(this._parameters.c1, this._geometry.e1P, this._geometry.e1) > 0
? 0
: 1,
sweepFlag: 1
});
const arc4 = {
x1: this._geometry.f1.x,
y1: this._geometry.f1.y,
x2: this._geometry.f0.x,
y2: this._geometry.f0.y,
x: this._geometry.e0.x,
y: this._geometry.e0.y
};
return [arc1, arc2, arc3, arc4];
}
get _pomaxCurves() {
const allCurves = this.bezierCurves.flat();
const allCurvesFormatted = [];
for (let i = 0; i < allCurves.length; i++) {
const origin =
i === 0
? this._geometry.e0
: { x: allCurves[i - 1].x, y: allCurves[i - 1].y };
const curve = allCurves[i];
const bezier = new Bezier(
origin.x,
origin.y,
curve.x1,
curve.y1,
curve.x2,
curve.y2,
curve.x,
curve.y
);
allCurvesFormatted.push(bezier);
}
return allCurvesFormatted;
}
get hasSelfIntersection() {
let interresects = false;
for (let i = 0; i < this._pomaxCurves.length; i++) {
const base = this._pomaxCurves[i];
for (let j = 0; j < this._pomaxCurves.length; j++) {
const current = this._pomaxCurves[j];
if (
!this._isSameBezier(base.points, current.points) &&
base.intersects(current).length > 0
) {
return true;
}
}
}
return false;
}
// I should make this much nicer...
_isSameBezier(b1, b2) {
return JSON.stringify(b1) === JSON.stringify(b2);
}
// JS adapted from https://imaginary-institute.com/resources/TechNote11/TechNote11.html
_getTangentPoint(a, b, r, side) {
const s = Vector2D.sub(b, a);
s.normalize();
const t = new Vector2D(s.y, -s.x);
const ab = a.dist(b);
const pb = Math.sqrt(ab * ab - r * r);
const beta = Math.atan2(pb, r);
const uscl = r * Math.cos(beta);
const vscl = r * Math.sin(beta);
const p0 = new Vector2D(
a.x + uscl * s.x + vscl * t.x,
a.y + uscl * s.y + vscl * t.y
);
const p1 = new Vector2D(
a.x + uscl * s.x - vscl * t.x,
a.y + uscl * s.y - vscl * t.y
);
const dP0 = Vector2D.sub(p0, a);
const dP1 = Vector2D.sub(p1, a);
const p0sgn = s.cross(dP0).z;
const p1sgn = s.cross(dP1).z;
if (p0sgn * p1sgn > 0) {
console.warn("getTangentPoint: both points on same side of line!");
return p0;
}
if (side === this._SIDE_RIGHT) {
if (p0sgn > 0) return p0;
return p1;
}
if (p0sgn < 0) return p0;
return p1;
}
setParameters(start, end, d, dP, a, b, aP, bP) {
this._parameters = {
c0: this.start.point.copy(),
c1: this.end.point.copy(),
r0: start.radius,
r1: end.radius,
d: d.copy(),
dP: dP.copy(),
a: a,
b: b,
aP: aP,
bP: bP
};
}
buildGeometry() {
this._geometry.e0 = this._getTangentPoint(
this._parameters.c0,
this._parameters.d,
this._parameters.r0,
this._SIDE_RIGHT
);
this._geometry.e1 = this._getTangentPoint(
this._parameters.c1,
this._parameters.d,
this._parameters.r1,
this._SIDE_LEFT
);
this._geometry.e0P = this._getTangentPoint(
this._parameters.c0,
this._parameters.dP,
this._parameters.r0,
this._SIDE_LEFT,
true
);
this._geometry.e1P = this._getTangentPoint(
this._parameters.c1,
this._parameters.dP,
this._parameters.r1,
this._SIDE_RIGHT
);
this._geometry.f0 = Vector2D.lerp(
this._geometry.e0,
this._parameters.d,
this._parameters.a
);
this._geometry.f1 = Vector2D.lerp(
this._geometry.e1,
this._parameters.d,
this._parameters.b
);
this._geometry.f0P = Vector2D.lerp(
this._geometry.e0P,
this._parameters.dP,
this._parameters.aP
);
this._geometry.f1P = Vector2D.lerp(
this._geometry.e1P,
this._parameters.dP,
this._parameters.bP
);
Object.keys(this._geometry).forEach((k) => {
if (!this._geometry[k].x || !this._geometry[k].y) {
this.isValid = false;
}
});
}
checkIntersection(target) {
return this._pomaxCurves.some((c) => {
let yes = false;
target._pomaxCurves.forEach((c1) => {
if (c1.intersects(c).length > 0) {
yes = true;
}
});
console.log(yes);
return yes;
});
}
buildPath() {
if (!this.isValid) {
console.warn("Glob is not valid, returning an empty path string.");
return "";
}
const [arc1, arc2, arc3, arc4] = this.bezierCurves;
let pathString = `M ${this._geometry.e0.x} ${this._geometry.e0.y} `;
arc1.forEach((c) => {
pathString += `C ${c.x1} ${c.y1} ${c.x2} ${c.y2} ${c.x} ${c.y} `;
});
pathString += `C ${arc2.x1} ${arc2.y1} ${arc2.x2} ${arc2.y2} ${arc2.x} ${arc2.y} `;
arc3.forEach((c) => {
pathString += `C ${c.x1} ${c.y1} ${c.x2} ${c.y2} ${c.x} ${c.y} `;
});
pathString += `C ${arc4.x1} ${arc4.y1} ${arc4.x2} ${arc4.y2} ${arc4.x} ${arc4.y} `;
pathString += "Z";
return pathString;
}
}
class Node {
constructor(opts) {
const defaults = {
x: 0,
y: 0,
radius: 24,
cap: "round"
};
const { x, y, radius, cap } = Object.assign(defaults, opts);
this.x = x;
this.y = y;
this.radius = radius;
this.cap = cap;
this.point = new Vector2D(this.x, this.y);
}
}
function createGrid(width, height, res) {
const colSize = width / res;
const rowSize = height / res;
const cells = [];
for (let x = 0; x < width; x += colSize) {
for (let y = 0; y < height; y += rowSize) {
cells.push({
x,
y,
width: colSize,
height: rowSize,
id: Math.random()
});
}
}
cells.forEach((cell) => {
// neighbors
const maxDist = Vector2D.dist(
new Vector2D(0, 0),
new Vector2D(-colSize, -rowSize)
);
const neighbors = cells.filter((c) => {
const dist = Vector2D.dist(
new Vector2D(c.x + c.width / 2, c.y + c.height / 2),
new Vector2D(cell.x + cell.width / 2, cell.y + cell.height / 2)
);
return dist <= maxDist && dist > 0;
});
cell.neighbors = neighbors;
});
return cells;
}
function render(index) {
const width = 768;
const height = 1024;
const svgElement = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
console.log(svgElement);
svgElement.classList.add("poster");
svgElement.innerHTML = `
<foreignObject x="0" y="664" width="100%" height="360" class="poster__text">
<div>
<h1></h1>
<p></p>
</div>
</foreignObject>
`;
document.body.appendChild(svgElement);
const svg = SVG(svgElement).viewbox(0, 0, width, height);
const graphics = svg.group();
const globGroup = svg.group();
let seed = pad(index, 4) || pad(random(0, 1000, true), 4);
seedPRNG(seed + 8);
svgElement.querySelector("h1").innerHTML = `“Union” — ${seed} / 1000`;
svgElement.querySelector("p:first-of-type").innerHTML =
"Generative poster design composed with Globs – a graceful shape drawn using two circles and two Bézier curves.";
const grid = createGrid(width, width, 3);
const padding = 64;
const globs = [];
const globCount = random(2, 4, true);
const black = "#0D0805";
const colorPalettes = [["#F6AE51", "#EF574D", "#5EBD8A", "#F19CAB"]];
const basePalette = random(colorPalettes);
const color1 = random(basePalette);
const color2 = random(basePalette.filter((c) => c !== color1));
const color3 = random(
basePalette.filter((c) => c !== color1 && c !== color2)
);
const weightedPalette = weightedRandom([
{
weight: 40,
value: black
},
{
weight: 20,
value: color1
},
{
weight: 20,
value: color2
},
{
weight: 20,
value: color3
}
]);
const chosenColors = new Set();
const pickColor = () => {
const choice = random(weightedPalette);
chosenColors.add(choice);
return choice;
};
for (let i = 0; i < globCount; i++) {
const choice = random(grid.filter((c) => !c.taken));
const neighbor = random(choice.neighbors.filter((n) => !n.taken));
if (!choice || !neighbor) continue;
choice.taken = true;
neighbor.taken = true;
const start = new Node({
x: choice.x + choice.width / 2,
y: choice.y + choice.height / 2,
radius: Math.min(choice.width, choice.height) / 2 - padding
});
const end = new Node({
x: neighbor.x + neighbor.width / 2,
y: neighbor.y + neighbor.height / 2,
radius: Math.min(neighbor.width, neighbor.height) / 2 - padding
});
const glob = new Glob({
start,
end,
a: 1,
b: 1,
aP: 1,
bP: 1
});
globs.push(glob);
}
graphics.clear();
globs.forEach((glob) => {
graphics.path(glob.buildPath()).fill(pickColor());
});
const cellWidth = grid[0].width;
grid.forEach((cell) => {
if (random(0, 1) > 0.5) {
const fill = cell.taken ? "#fff" : pickColor();
graphics
.circle(32)
.cx(cell.x + cell.width / 2)
.cy(cell.y + cell.height / 2)
.fill(fill);
}
});
[...chosenColors]
.filter((c) => c !== black)
.forEach((c, i) => {
graphics
.circle(32)
.cx(width - cellWidth / 2)
.y(height - padding - 32 - 14)
.fill(c)
.translate(i * 24, 0)
.stroke({ width: 2, color: "#fff" });
});
}
document.addEventListener("click", render);
let end = 2;
let index = 6;
for (let i = 1; i < 6; i++) {
render(i);
}
const buds = document.querySelector("body");
setInterval(() => {
const y = window.pageYOffset;
const windowHeight = window.innerHeight;
if (y + windowHeight * 8 >= buds.scrollHeight) {
for (let i = index; i < index + 3; i++) {
if (i <= 1000) {
render(i);
}
}
index += 3;
}
}, 100);
function setWindowHeight() {
document.documentElement.style.setProperty(
"--window-height",
`${window.innerHeight}px`
);
}
function pad(n, width, z) {
z = z || "0";
n = n + "";
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
function weightedRandom(opts) {
const items = [];
opts.forEach((opt) => {
for (let i = 0; i < opt.weight; i++) {
items.push(opt.value);
}
});
return items;
}
This Pen doesn't use any external CSS resources.