Created
June 4, 2020 16:23
-
-
Save JakeCoxon/62cc0b0d46ca407bfb70988633686848 to your computer and use it in GitHub Desktop.
Procedural symbols
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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