random-shapes
Version:
Generate random shapes!
348 lines (294 loc) • 10.1 kB
JavaScript
const seedrandom = require('seedrandom');
let randGen = seedrandom();
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return `${this.x.toFixed(2)} ${this.y.toFixed(2)}`
}
}
function rnd(boundMin, boundMax) {
return boundMin + (randGen() * (boundMax - boundMin))
}
function truncate(pos, posMin, posMax) {
if (posMin == null) posMin = Number.NEGATIVE_INFINITY
if (posMax == null) posMax = Number.POSITIVE_INFINITY
return Math.min(Math.max(pos, posMin), posMax)
}
function movePoint(pt, rho, r, { xMin, xMax, yMin, yMax }={}) {
return new Point(truncate(pt.x + r * Math.cos(rho), xMin, xMax),
truncate(pt.y + r * Math.sin(rho), yMin, yMax))
}
function compareArrays(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i ++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function isAuto(entry) {
return (entry == null || entry === "auto")
}
function preProcessOverride(width, height, opt, override) {
// There are essentially two modes: exact ("p" and "r" modes) and auto (null and "w" modes).
// So we can first convert all the p's to r's, and all the nulls to w's (with default window size).
for (let i = 0; i < override.length; i ++) {
if (isAuto(override[i])) {
override[i] = {x: null, y: null, angle: null}
}
["x", "y", "angle"].forEach(k => {
if (isAuto(override[i][k])) {
if (k === "angle") {
override[i][k] = ["w", opt.angleWindowSize]
}
else {
override[i][k] = ["w", opt.posWindowSize]
}
if (i === 0)
if (k === "x") {
override[i].x = ["r", 0, 0]
}
if (i === override.length - 1)
if (k === "x") {
override[i].x = ["r", width, width]
}
}
else if (override[i][k][0] === "p") {
override[i][k] = ["r", override[i][k][1], override[i][k][1]]
}
})
}
}
function checkOverride(width, opt, override) {
// some checks
if (override.length !== opt.numControls) {
console.warn("Number of control points and the length of the override array are not equal."
+ " " + "The numControls option will be disregarded.")
opt.numControls = override.length
}
// the endpoints must be ["r", x, x]
if (!compareArrays(override[0].x, ["r", 0, 0])) {
console.error("The first element of override array must have x property of [0, 0].")
}
if (!compareArrays(override[opt.numControls - 1].x, ["r", width, width])) {
console.error("The first element of override array must have x property of [0, 0].")
}
}
function convertEndPoints(opt, override) {
if (override[0].y[0] === "w")
override[0].y = ["r", opt.leftPos - override[0].y[1]/2, opt.leftPos + override[0].y[1]/2]
if (override[opt.numControls - 1].y[0] === "w")
override[opt.numControls - 1].y = ["r", opt.leftPos - override[opt.numControls - 1].y[1]/2, opt.leftPos + override[opt.numControls - 1].y[1]/2]
}
function convertInteriorPoints(width, height, opt, override, initX, slope, intercept) {
for (let i = 0; i < override.length; i ++) {
["x", "y", "angle"].forEach(k => {
if (override[i][k][0] === "w") {
let center = null
let wSize = null
let minVal = null
let maxVal = null
switch (k) {
case "angle":
center = Math.atan(slope)
wSize = opt.angleWindowSize
minVal = Number.NEGATIVE_INFINITY
maxVal = Number.POSITIVE_INFINITY
break;
case "x":
center = initX[i]
wSize = opt.posWindowSize
minVal = 0
maxVal = width
break;
case "y":
center = initX[i] * slope + intercept
wSize = opt.posWindowSize
minVal = 0
maxVal = height
break;
}
override[i][k] = ["r", truncate(center - wSize/2, minVal, maxVal), truncate(center + wSize/2, minVal, maxVal)]
}
})
}
}
function getMinDistance(a) {
let minDistance = Number.POSITIVE_INFINITY
for (let i = 0; i < a.length - 1; i ++) {
minDistance = Math.min(
minDistance,
Math.sqrt(Math.pow(a[i].point.x - a[i+1].point.x, 2) + Math.pow(a[i].point.y - a[i+1].point.y, 2))
)
}
return minDistance
}
function getRange(maxNum) {
return Array.from(new Array(maxNum), (x, i) => i)
}
function genCurve(width, height, opt, override, initX) {
// init data array
let data = Array(opt.numControls)
for (let i = 0; i < opt.numControls; i ++) {
data[i] = {initX: initX[i]}
}
// initial slope... first figure out final y's of endpoints
const finalLeft = rnd(override[0].y[1], override[0].y[2])
const finalRight = rnd(override[opt.numControls - 1].y[1], override[opt.numControls - 1].y[2])
const slope = (finalRight - finalLeft) / width
override[0].y = ["r", finalLeft, finalLeft]
override[opt.numControls - 1].y = ["r", finalRight, finalRight]
convertInteriorPoints(width, height, opt, override, initX, slope, finalLeft)
for (let i = 0; i < opt.numControls; i ++) {
data[i].angle = rnd(override[i].angle[1], override[i].angle[2])
data[i].point = new Point(rnd(override[i].x[1], override[i].x[2]), rnd(override[i].y[1], override[i].y[2]))
}
if (opt.debug)
console.log("data", JSON.parse(JSON.stringify(data)))
const distance = getMinDistance(data)
for (let i = 0; i < opt.numControls; i ++) {
data[i].ctrl = movePoint(data[i].point, data[i].angle, -1*distance/2)
data[i].ctrl_alt = movePoint(data[i].point, data[i].angle, distance/2)
}
if (opt.debug)
console.log("data with controls", data)
let curve = "M 0 " + data[0].point.y.toFixed(2) + " C " + data[0].ctrl_alt + ", " + data[1].ctrl + ", " + data[1].point + " "
for (let i = 2; i < opt.numControls; i ++) {
curve += "S " + data[i].ctrl + ", " + data[i].point + " "
}
return {data: data, curve: curve}
}
export function genHLines(width, height, options, override) {
// Override is an array of objects.
// If the entry at position i is null, undefined, or "auto", then default is applied.
// Each non-auto entry is an object with 3 possible keys: x, y, and angle.
// Each key has a value that's an array (or null). The first element of the array
// is the "mode" of overriding. There are 3 possible (non-null) modes:
// - null, undefined, or "auto"
// - ["p", value]: specify the exact value
// - ["w", value]: specify the size of the window
// - ["r", l_bound, u_bound]: specify the minimum and maximum values
const opt = {
leftPos: 0.5*height,
rightPos: 0.5*height,
posWindowSize: 0.2*height,
angleWindowSize: Math.PI/3,
numControls: 2,
seed: '',
debug: false,
...options
}
if (opt.seed !== '') {
randGen = seedrandom(opt.seed)
if (opt.debug)
console.log('seeded with string: ', opt.seed)
}
else {
randGen = seedrandom()
if (opt.debug)
console.log('no seed')
}
if (opt.debug)
console.log("inital opt", JSON.parse(JSON.stringify(opt)))
// preprocess override
if (!override)
override = Array(opt.numControls).fill("auto")
preProcessOverride(width, height, opt, override)
checkOverride(width, opt, override)
convertEndPoints(opt, override)
if (opt.debug)
console.log("post-processed override", JSON.parse(JSON.stringify(override)))
let initX = getRange(opt.numControls).map(x => x / (opt.numControls - 1) * width)
if (opt.debug)
console.log("initX", JSON.parse(JSON.stringify(initX)))
let lastFixed = 0
for (let i = 1; i < opt.numControls; i ++) {
if (opt.debug) {
console.log("---", i)
}
if (override[i].x[0] === "r") {
initX[i] = (override[i].x[1] + override[i].x[2]) / 2
if (opt.debug) {
console.log(i, "is in r mode...")
console.log(i, initX[i])
}
if (i - lastFixed > 1) {
// do linear interpolation from the last fixed point
const lengthInBetween = (initX[i] - initX[lastFixed]) / (i - lastFixed)
for (let j = lastFixed + 1; j < i; j ++) {
initX[j] = initX[j - 1] + lengthInBetween
if (opt.debug) {
console.log(j, initX[j])
}
}
}
lastFixed = i
}
}
if (opt.debug){
console.log("initX at the end", initX)
}
const r = getRange(opt.numLines).map(i => {
let tmp_override = JSON.parse(JSON.stringify(override))
return genCurve(width, height, opt, tmp_override, initX)
})
return(r)
}
function genOneBlob(rand, size, initRadius, distance, opt) {
const tmp = rand() * 2 * Math.PI;
const initAngle = getRange(opt.numControls).map(x => tmp + x/opt.numControls*2*Math.PI)
const center = new Point(size/2, size/2)
let data = Array(opt.numControls)
for (let i = 0; i < opt.numControls; i ++) {
data[i] = {point: movePoint(
movePoint(center, initAngle[i], initRadius),
rand() * Math.PI*2,
rand() * opt.posWindowSize)}
data[i].angle = rnd(initAngle[i] + Math.PI/2 - opt.angleWindowSize/2, initAngle[i] + Math.PI/2 + opt.angleWindowSize/2)
data[i].ctrl = movePoint(data[i].point, data[i].angle, -1*rnd(distance*(1-opt.handleWindowSize), distance*(1+opt.handleWindowSize)))
data[i].ctrl_alt = movePoint(data[i].point, data[i].angle, rnd(distance*(1-opt.handleWindowSize), distance*(1+opt.handleWindowSize)))
}
let path = "M " + data[0].point + " "
+ "C " + data[0].ctrl_alt + ", " + data[1].ctrl + ", " + data[1].point + " "
for (let i = 2; i < opt.numControls; i ++) {
path += "S " + data[i].ctrl + ", " + data[i].point + " "
}
path += "S " + data[0].ctrl + ", " + data[0].point
return({path: path, data: data})
}
export function genBlob(size, options) {
return genHBlobs(size, options)[0]
}
export function genHBlobs(size, options) {
const opt = {
numBlobs: 1,
numControls: 3,
posWindowSize: 0.1*size,
angleWindowSize: Math.PI/3,
handleWindowSize: 0.5,
seed: '',
debug: false,
...options
}
if (opt.seed !== '') {
randGen = seedrandom(opt.seed)
if (opt.debug)
console.log('seeded with string: ', opt.seed)
}
else {
randGen = seedrandom()
if (opt.debug)
console.log('no seed')
}
const r = getRange(opt.numBlobs).map(i => {
const initRadius = size/2 - 2*opt.posWindowSize
const distance = 2*Math.PI*initRadius / opt.numControls / 2.5
return genOneBlob(randGen, size, initRadius, distance, opt)
})
return(r)
}
export const test = () => {
return "abcdef"
}