Skip to content

Instantly share code, notes, and snippets.

@JakeCoxon
Created June 4, 2020 16:23
Show Gist options
  • Save JakeCoxon/62cc0b0d46ca407bfb70988633686848 to your computer and use it in GitHub Desktop.
Save JakeCoxon/62cc0b0d46ca407bfb70988633686848 to your computer and use it in GitHub Desktop.
Procedural symbols
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<link rel='stylesheet' href="styles.css"/>
</head>
<body>
<script src="https://cdn.jsdelivr.net/lodash/4/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.0/seedrandom.min.js"></script>
<div class='header' a='2'>
<div class='left'>
<div id='logo'></div>
</div>
<div class='right'>
<h1>Procedural symbols - <a class="subheader" href="http://jake.cx">Jake Coxon</a></h1>
<p>Here is an algorithm I created to produce randomly generated symbols. You can imagine these symbols to be some kind of alien language, hieroglyphs or runes from an ancient civilisation.</p>
<p>The symbols are made up of a differing number of lines placed at random points on a grid with a bias towards points of existing lines. This collection of lines is then duplicated with a random choice of mirrored or rotational symmetry to make the symbols more interesting.</p>
</div>
</div>
<div id='generationcontainer'></div>
<button id='morebtn'>More</button>
<script src="script.js"></script>
</body>
</html>
//jshint esnext:true
function pick(array) {
return array[Math.floor(Math.random() * array.length)];
}
function weighted(weighting, max) {
const sum = _.range(weighting).map(x => Math.random()).reduce((x, y) => x + y);
return sum / weighting * max;
}
function lerp(x, a, b) {
return x * (b - a) + a;
}
function randomOnGrid(min, max, gridNum) {
return lerp(
Math.floor(Math.random() * (gridNum + 1)) / gridNum,
min, max
);
}
function mirrorLineX(line) {
return {
x1: 1 - line.x1,
y1: line.y1,
x2: 1 - line.x2,
y2: line.y2
};
}
function mirrorLineY(line) {
return {
x1: line.x1,
y1: 1 - line.y1,
x2: line.x2,
y2: 1 - line.y2
};
}
function makeLine([x1, y1], [x2, y2]) {
return { x1, y1, x2, y2 };
}
function point1(line) {
return [line.x1, line.y1];
}
function point2(line) {
return [line.x2, line.y2];
}
function pointEq([x1, y1], [x2, y2]) {
return x1 == x1 && x2 == x2;
}
function lineToVec({ x1, y1, x2, y2 }) {
const dx = x2 - x1;
const dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy)
return [dx / len, dy / len]
}
function findDot([x1, y1], [x2, y2]) {
return x1 * x2 + y1 * y2;
}
function inclusiveRange(from, to, step) {
return _.range(from, to + step, step);
}
function centerLines(lines) {
let boundsLeft = Infinity,
boundsTop = Infinity,
boundsRight = 0,
boundsBottom = 0;
for (let line of lines) {
boundsLeft = Math.min(boundsLeft, Math.min(line.x1, line.x2));
boundsTop = Math.min(boundsTop, Math.min(line.y1, line.y2));
boundsRight = Math.max(boundsRight, Math.max(line.x1, line.x2));
boundsBottom = Math.max(boundsBottom, Math.max(line.y1, line.y2));
}
const offX = (1 - (boundsRight - boundsLeft)) / 2 - boundsLeft;
const offY = (1 - (boundsBottom - boundsTop)) / 2 - boundsTop;
return lines.map(line => ({
x1: line.x1 + offX,
y1: line.y1 + offY,
x2: line.x2 + offX,
y2: line.y2 + offY,
}))
}
function scaleLines(lines) {
let boundsLeft = Infinity,
boundsTop = Infinity,
boundsRight = 0,
boundsBottom = 0;
for (let line of lines) {
boundsLeft = Math.min(boundsLeft, Math.min(line.x1, line.x2));
boundsTop = Math.min(boundsTop, Math.min(line.y1, line.y2));
boundsRight = Math.max(boundsRight, Math.max(line.x1, line.x2));
boundsBottom = Math.max(boundsBottom, Math.max(line.y1, line.y2));
}
const offX = (1 - (boundsRight - boundsLeft)) / 2 - boundsLeft;
const offY = (1 - (boundsBottom - boundsTop)) / 2 - boundsTop;
return lines.map(line => ({
x1: (line.x1 - boundsLeft) / (boundsRight - boundsLeft),// + offX,
y1: (line.y1 - boundsTop) / (boundsBottom - boundsTop),
x2: (line.x2 - boundsLeft) / (boundsRight - boundsLeft),
y2: (line.y2 - boundsTop) / (boundsBottom - boundsTop),
}));
}
function rotateLines45(lines) {
const rotation = Math.PI / 4;
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
function rotate([x, y]) {
const lx = x - 0.5;
const ly = y - 0.5;
const nx = lx * cos - ly * sin;
const ny = lx * sin + ly * cos;
return [nx + 0.5, ny + 0.5];
}
return lines.map(line => {
const p1 = rotate(point1(line));
const p2 = rotate(point2(line));
return makeLine(p1, p2);
});
}
function rotateLine90(line) {
return {
x1: 1 - line.y1,
y1: line.x1,
x2: 1 - line.y2,
y2: line.x2
};
}
function rotateLine180(line) {
return rotateLine90(rotateLine90(line));
}
function rotateLines90(lines) {
return lines.map(line => {
return {
x1: 1 - line.y1,
y1: line.x1,
x2: 1 - line.y2,
y2: line.x2
};
});
}
function rotateLines180(lines) {
return rotateLines90(rotateLines90(lines));
}
function rotatePoint90([x, y]) {
return [1 - y, x];
}
function rotatePoint180(point) {
return rotatePoint90(rotatePoint90(point));
}
function mirrorPointX([x, y]) {
return [1 - x, y]
}
function mirrorPointY([x, y]) {
return [x, 1- y]
}
function mirrorLinesX(lines) {
return lines.map(mirrorLineX);
}
function mirrorLinesY(lines) {
return lines.map(mirrorLineY);
}
function pointClose([x1, y1], [x2, y2]) {
return Math.abs(x2 - x1) < 0.1 &&
Math.abs(y2 - y1) < 0.1;
}
function isLinesIntersecting(line1, line2) {
const [l1x1, l1y1] = point1(line1);
const [l1x2, l1y2] = point2(line1);
const [l2x1, l2y1] = point1(line2);
const [l2x2, l2y2] = point2(line2);
const d = (l2y2 - l2y1) * (l1x2 - l1x1)
- (l2x2 - l2x1) * (l1y2 - l1y1);
const n_a = (l2x2 - l2x1) * (l1y1 - l2y1)
- (l2y2 - l2y1) * (l1x1 - l2x1);
const n_b = (l1x2 - l1x1) * (l1y1 - l2y1)
- (l1y2 - l1y1) * (l1x1 - l2x1);
if (Math.abs(d) < 0.00001) {
const l1minx = Math.min(l1x1, l1x2);
const l1miny = Math.min(l1y1, l1y2);
const l2minx = Math.min(l2x1, l2x2);
const l2miny = Math.min(l2y1, l2y2);
const l1maxx = Math.max(l1x1, l1x2);
const l1maxy = Math.max(l1y1, l1y2);
const l2maxx = Math.max(l2x1, l2x2);
const l2maxy = Math.max(l2y1, l2y2);
if (l1maxx < l2minx) return false;
if (l1minx > l2maxx) return false;
if (l1maxy < l2miny) return false;
if (l1miny > l2maxy) return false;
return true;
}
const ua = n_a / d;
const ub = n_b / d;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return true;
}
return false;
}
function isColinear(line1, line2) {
const [l1x1, l1y1] = point1(line1); // A
const [l1x2, l1y2] = point2(line1); // D
const [l2x1, l2y1] = point1(line2); // B
const [l2x2, l2y2] = point2(line2); // E
const f = [l2x1 - l1x1, l2y1 - l1y1];
const e = [l2x2, l2y2];
return dot(cross(f, e), cross(f, e)) / dot(f, f) * dot(e, e) < 0.0001;
}
function dot([x1, y1], [x2, y2]) {
return x1 * x2 + y1 * y2;
}
function cross([x1, y1], [x2, y2]) {
return x1 * y2 - y1 * x2;
}
function isPointOnLine(point, line) {
const [p1x, p1y] = point1(line);
const [p2x, p2y] = point2(line);
const [ax, ay] = [p2x - p1x, p2y - p1y];
const [bx, by] = [point.x - p1x, point.y - p1y];
return cross([ax, ay], [bx, by]) < 0.0001;
}
function isLineTooClose(line, otherLines) {
const { x1, y1, x2, y2 } = line;
if (x2 == x1 && y2 == y1) return true;
const v1 = lineToVec(line);
for (let otherLine of otherLines) {
const intersect = isLinesIntersecting(line, otherLine);
if (intersect) {
const v2 = lineToVec(otherLine);
const dot = findDot(v1, v2);
if (Math.abs(dot) > 0.70) return true;
}
}
return false;
}
function translateLinesX(lines) {
return lines.map(line => ({
x1: line.x1 + 0.5,
y1: line.y1,
x2: line.x2 + 0.5,
y2: line.y2,
}));
}
function pickWeighted(array) {
const c = _.chunk(array, 2);
const sum = _.sumBy(c, x => x[0]);
let r = Math.random();
for (let [weight, item] of c) {
if (r < weight / sum) {
return item;
}
r -= weight/sum;
}
}
function containsPoint(array, point) {
return _.some(array, p => pointEq(p, point))
}
function createSymbol(symbolId) {
Math.seedrandom(`hello${symbolId}`);
let lines = [];
const halfX = Math.random() < 0.99;
const halfY = halfX && Math.random() < 0.4;
const availTransforms = (halfX && halfY) ?
[
0.5, [rotateLines90, rotateLines180],
//0.1, [rotateLines180],
0.1, [translateLinesX, rotateLines180],
0.1, [translateLinesX, mirrorLinesY],
1, [mirrorLinesX, mirrorLinesY]
]
: (halfX && !halfY) ?
[1, [mirrorLinesX],
1, [rotateLines180],
0.1, [translateLinesX]]
: [1, []]
const transforms = pickWeighted(availTransforms);
const allowOverlap = Math.random() < 0.3;
const pointsX = halfX && allowOverlap ?
() => randomOnGrid(0, 0.6, 6)
: halfX ? () => randomOnGrid(0, 0.5, 5)
: () => randomOnGrid(0, 1, 10);
const pointsY = halfY && allowOverlap ?
() => randomOnGrid(0, 0.6, 6)
: halfY ? () => randomOnGrid(0, 0.5, 5)
: () => randomOnGrid(0, 1, 10);
const pickPoints = [];
const line = {
x1: pointsX(),
y1: pointsY(),
x2: pointsX(),
y2: pointsY()
}
lines.push(line);
pickPoints.push(point1(line));
pickPoints.push(point2(line));
let numExtra = Math.max(weighted(4, 12) - 2, 1);
if (Math.random() < 0.01) {
numExtra = 30;
// console.log(symbolId);
}
// if (symbolId > 4000)
// numExtra = weighted(5, Math.min(Math.floor(symbolId / 4000) + 3, 32));
for (let i of _.range(numExtra)) {
let j = 10;
while(j--) {
const start = Math.random() < 0.95 ?
pick(pickPoints)
: [pointsX(), pointsY()];
const end = Math.random() < 0.4 ?
pick(pickPoints)
: [pointsX(), pointsY()];
const line = makeLine(start, end);
if (Math.random() < 0.99999 && isLineTooClose(line, lines)) {
continue;
}
lines.push(line);
if (!containsPoint(pickPoints, start)) {
pickPoints.push(start);
}
pickPoints.push(end);
break;
}
}
let i = 0;
for (let transform of transforms) {
lines.push(...transform(lines))
if (i < transforms.length - 1) {
_.remove(lines, x => Math.random() < 0.005);
}
i++;
};
if (Math.random() < 0.01) {
lines.push(...rotateLines45(lines));
}
if (Math.random() < 0.5) {
lines = rotateLines90(lines)
}
if (Math.random() < 0.1) {
// lines = rotateLines45(lines);
}
const dots = [];
if (Math.random() < 0.05) {
dots.push({ center: [pointsX(), pointsY()], r: 0.05 });
if (Math.random() < 0.4) {
dots.push({ center: [pointsX(), pointsY()], r: 0.05 });
}
}
lines = scaleLines(lines);
return {
lines: lines,
dots: dots
}
}
function attrs(el, map) {
_.forEach(map, (val, key) => {
if (key == 'style') Object.assign(el.style, val)
else el.setAttribute(key, val);
});
}
function symbolToSvg(symbol, label, size) {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
//c.style.margin = '10px'
attrs(c, {
style: {
display: 'inline-block',
width: size + 'px',
height: size + 'px',
backgroundColor: '',
},
width: size,
height: size,
viewBox: `0 0 ${size} ${size}`
})
if (label != undefined) {
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
attrs(t, {
x: 0,
y: 8,
'font-family': 'Georgia',
'font-size': 8,
'fill': '#aaa'
});
var textNode = document.createTextNode(label);
t.appendChild(textNode)
c.appendChild(t);
}
const margin = 18;
const layout = (pos) => pos * (size - margin * 2) + margin
symbol.lines.forEach(line => {
const l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
'x1 y1 x2 y2'.split(' ').forEach(a => {
l.setAttribute(a, layout(line[a]));
})
attrs(l, {
'stroke-width': size / 41,
'stroke': '#333'
})
c.appendChild(l)
})
symbol.dots.forEach(dot => {
const o = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
attrs(o, {
cx: layout(dot.center[0]),
cy: layout(dot.center[1]),
r: dot.r * (size - margin * 2),
'fill': '#333'
})
c.appendChild(o)
})
return c;
}
function generateAndAddToDocument(container, startIndex, numSymbols) {
const symbols = _.range(numSymbols).map(x => createSymbol(startIndex + x));
function add(symbol, index, size) {
const svg = symbolToSvg(symbol, index, size);
container.appendChild(svg);
}
let i = startIndex;
for(let symbol of symbols) {
add(symbol, i, 64);
i++
}
}
const logoId = Math.floor(Math.random() * 100000000);
const logo = document.getElementById('logo')
logo.appendChild(
symbolToSvg(createSymbol(logoId), logoId, 84)
)
logo.addEventListener('click', () => {
let logoId = prompt("Input a number")
if (logoId != undefined && (logoId = Number(logoId))) {
logo.innerHTML = "";
logo.appendChild(
symbolToSvg(createSymbol(logoId), logoId, 84)
)
}
})
let startIndex = 0;
const container = document.getElementById('generationcontainer')
generateAndAddToDocument(container, 0, 1024);
document.getElementById('morebtn').addEventListener('click', () => {
startIndex += 1024;
generateAndAddToDocument(container, startIndex, 1024);
})
body {
font-family: Helvetica, Arial;
font-weight: 100;
line-height: 1.2;
background-color: #fafafa;
color: #333;
}
p {
}
h1 {
font-weight: 100;
line-height: 1;
margin: 10px 0;
}
#logo {
display: inline-block;
vertical-align: middle;
margin: 0 10px;
}
.header {
display: flex;
flex-direction: row;
max-width: 800px;
margin: auto;
}
.left {
}
.right {
flex: 1;
}
.subheader {
font-size: 0.6em;
}
#generationcontainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
#morebtn {
width: 100%;
font-size: 18px;
font-family: Helvetica, Arial;
padding: 20px;
border: 0;
background-color: #eee;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment