neuralaesthetics
Version:
Awesome neural networks for aesthetic web pages
598 lines (597 loc) • 23.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import document from "../Utils/document";
import { delay } from "../Utils/general";
import * as d3 from "d3";
import { assignRandomPositions, generateNeuron, generateConnProps, } from "../Utils/generator";
class Params {
constructor() {
this.svgWidth = 0;
this.svgHeight = 0;
this.running = false;
this.refresh = false;
this.checkSvg = true;
this.distanceLayers = 100;
this.distanceNeurons = 50;
this.iter = 0;
this.hidden = false;
this.document = undefined;
}
}
export class BasePainter extends Params {
constructor(htmlElement) {
super();
// since the neuron number will not change much if at all
this.connections = [];
this.minX = 0;
this.maxX = 0;
this.minY = 0;
this.maxY = 0;
this.center = {
x: 0,
y: 0,
};
this.document = document;
if (this.document !== "undefined" &&
typeof this.document.hidden !== "undefined") {
// Add a visibility change event listener
this.document.addEventListener("visibilitychange", () => {
this.hidden = this.document.hidden;
});
}
this.neurons = [];
this.running = false;
this.svgElement = htmlElement;
this.calculateSVGSizes();
}
set neuronRadius(value) {
for (let idx = 0; idx < this.neurons.length; idx += 1) {
this.neurons[idx].radius = value;
}
}
set neuronStrokeWidth(value) {
for (let idx = 0; idx < this.neurons.length; idx += 1) {
this.neurons[idx].strokeWidth = value;
}
}
set neuronStrokeColor(value) {
for (let idx = 0; idx < this.neurons.length; idx += 1) {
this.neurons[idx].strokeColor = value;
}
}
set neuronBgColor(value) {
for (let idx = 0; idx < this.neurons.length; idx += 1) {
this.neurons[idx].bgColor = value;
}
}
set connectionStrokeWidth(value) {
for (let idx = 0; idx < this.connections.length; idx += 1) {
this.connections[idx].strokeWidth = value;
}
}
set connectionStrokeColor(value) {
for (let idx = 0; idx < this.connections.length; idx += 1) {
this.connections[idx].strokeColor = value;
}
}
set connectionStrokeOpacity(value) {
for (let idx = 0; idx < this.connections.length; idx += 1) {
this.connections[idx].strokeOpacity = value;
}
}
setPosition(index, posX, posY) {
this.neurons[index].posX = posX;
this.neurons[index].posY = posY;
}
setPositions(neuronArr) {
for (let posObj of neuronArr) {
// pos will be an object with an index, posX and posY
this.setPosition(posObj.index, posObj.posX, posObj.posY);
}
}
generateNeurons(numberNeurons) {
// generates boilerplate neurons for the classp
// assumes neurons array is empty
for (let idx = 0; idx < numberNeurons; idx++) {
let neuron = generateNeuron();
neuron.index = idx;
this.neurons.push(neuron);
}
}
addNeuron(neuron) {
// adds more neurons to the existing ones
this.neurons.push(neuron);
}
addFullConnections() {
// adds connections between all neurons
let connections = [];
for (let idx = 0; idx < this.neurons.length; idx++) {
for (let idx2 = idx + 1; idx2 < this.neurons.length; idx2++) {
let connection = {
index: connections.length,
idxNeuron1: idx,
idxNeuron2: idx2,
};
connections.push(connection);
}
}
this.addConnections(connections);
}
addConnections(connections) {
// validating and adding missing props
let basicConn = generateConnProps();
for (let idx = 0; idx < connections.length; idx++) {
if (connections[idx].strokeWidth === undefined) {
connections[idx].strokeWidth = basicConn.strokeWidth;
}
if (connections[idx].strokeColor === undefined) {
connections[idx].strokeColor = basicConn.strokeColor;
}
if (connections[idx].strokeOpacity === undefined) {
connections[idx].strokeOpacity = basicConn.strokeOpacity;
}
connections[idx].index = idx + this.connections.length;
}
this.connections.push(...connections);
}
assignRandomPosition() {
assignRandomPositions(this.neurons, this.minX, this.maxX, this.minY, this.maxY);
}
addArg(index, key, flag) {
this.neurons[index].flags[key] = flag;
}
generateNeuronLayers(params) {
let nrNeurons = params.layers.reduce((prevVal, currVal) => prevVal + currVal);
this.generateNeurons(nrNeurons);
this.arrangeInLayers(params);
}
arrangeCustom(numNeurons, customArrangement) {
this.generateNeurons(numNeurons);
customArrangement(this.neurons);
}
arrangeInLayers(params) {
this.distanceLayers = params.distanceLayers;
this.distanceNeurons = params.distanceNeurons;
// arranges existent neurons in layers
// the network will be centered around in the middle of the svg or group
// calculating X positions based on layerDistance
let unit = params.distanceLayers;
let neuronUnit = params.distanceNeurons;
let layerNumber = params.layers.length;
let middle = (layerNumber - 1) / 2;
let neuronIdx = 0;
for (let idx = 0; idx < layerNumber; idx += 1) {
// keeping index of neurons because they should be in order
let layerNr = 0;
for (; layerNr < params.layers[idx]; layerNr += 1) {
let layerMiddle = (params.layers[idx] - 1) / 2;
this.neurons[neuronIdx + layerNr].flags["layer"] = idx;
this.neurons[neuronIdx + layerNr].flags["layerIdx"] = layerNr;
//formula for centering the neurons in the svg
this.neurons[neuronIdx + layerNr].posX = unit * (idx - middle);
this.neurons[neuronIdx + layerNr].posY = unit * (layerNr - layerMiddle);
}
neuronIdx += layerNr;
}
}
checkParams(params, mustHave) {
let missing = mustHave.filter((item) => !Object.keys(params).includes(item));
if (missing.length > 0) {
throw new Error(`missing params: ${missing}`);
}
}
arrangeInCircle(params) {
this.checkParams(params, ["neurons", "radius"]);
// arranges neurons in a circle
this.generateNeurons(params.neurons);
const numNeurons = params.neurons; // The number of neurons you want to place on the circle
const radius = params.radius; // The radius of the circle
for (let i = 0; i < numNeurons; i++) {
const angle = (2 * Math.PI * i) / numNeurons; // Calculate the angle for this neuron
const x = radius * Math.cos(angle); // Calculate the x position
const y = radius * Math.sin(angle); // Calculate the y position
this.neurons[i].posX = x;
this.neurons[i].posY = y;
}
}
calculateSVGSizes() {
// if the svg size changes it will try to adjust the neuron positions accordingly
// Calculate the min and max values of the x and y coordinates
const rect = this.svgElement.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
this.svgWidth = width;
this.svgHeight = height;
this.minX = 0;
this.maxX = width;
this.minY = 0;
this.maxY = height;
this.center.x = (this.maxX + this.minX) / 2;
this.center.y = (this.maxY + this.minY) / 2;
}
drawInitialConnections() {
// draws the connections between the neurons
// appending a g element for connections to the root svg
d3.select(this.svgElement).append("g").attr("id", "connections");
// appending the lines to the g element
d3.select(this.svgElement)
.selectChildren("#connections")
.selectAll("line")
.data(this.connections, (el) => {
return el.index;
// return the index of the connection
})
.enter()
.append("line")
.attr("x1", (el) => {
return this.neurons[el.idxNeuron1].posX + this.center.x;
})
.attr("y1", (el) => {
return this.neurons[el.idxNeuron1].posY + this.center.y;
})
.attr("x2", (el) => {
return this.neurons[el.idxNeuron2].posX + this.center.x;
})
.attr("y2", (el) => {
return this.neurons[el.idxNeuron2].posY + this.center.y;
})
.attr("stroke", (el) => {
return el.strokeColor;
})
.attr("stroke-width", (el) => {
return el.strokeWidth;
})
.attr("stroke-opacity", (el) => {
return el.strokeOpacity;
});
}
drawInitialNeurons() {
// draws the neurons
d3.select(this.svgElement)
.selectAll("circle")
.data(this.neurons, (el) => {
return el.index;
// return the index of the neuron
})
.enter()
.append("circle")
.attr("cx", (el) => {
return el.posX + this.center.x;
})
.attr("cy", (el) => {
return el.posY + this.center.y;
})
.attr("r", (el) => {
return el.radius;
})
.attr("fill", (el) => {
return el.bgColor;
})
.attr("stroke", (el) => {
return el.strokeColor;
})
.attr("stroke-width", (el) => {
return el.strokeWidth;
});
// draws the connections
}
applyPositions() {
// applies newPos to pos for all neurons
for (let neuron of this.neurons) {
neuron.posX = neuron.newPosX;
neuron.posY = neuron.newPosY;
}
}
instantTransition() {
// same as transitionNeurons but without the transition
d3.select(this.svgElement)
.selectAll("circle")
.data(this.neurons, (el) => {
return el.index;
// return the index of the neuron
})
.attr("cx", (el) => {
return el.posX + this.center.x;
})
.attr("cy", (el) => {
return el.posY + this.center.y;
});
d3.select(this.svgElement)
.selectAll("line")
.data(this.connections, (el) => {
return el.index;
// return the index of the connection
})
.attr("x1", (el) => {
return this.neurons[el.idxNeuron1].posX + this.center.x;
})
.attr("y1", (el) => {
return this.neurons[el.idxNeuron1].posY + this.center.y;
})
.attr("x2", (el) => {
return this.neurons[el.idxNeuron2].posX + this.center.x;
})
.attr("y2", (el) => {
return this.neurons[el.idxNeuron2].posY + this.center.y;
})
.attr("stroke", (el) => {
return el.strokeColor;
})
.attr("stroke-width", (el) => {
return el.strokeWidth;
})
.attr("stroke-opacity", (el) => {
return el.strokeOpacity;
});
}
checkSvgSize() {
return __awaiter(this, void 0, void 0, function* () {
// if svg size changes, we need to recalculate the center with the calculateSVGSizes function
while (true && this.checkSvg) {
yield delay(50);
if (this.running) {
const rect = this.svgElement.getBoundingClientRect();
if (rect.width != this.svgWidth || rect.height != this.svgHeight) {
this.calculateSVGSizes();
/* since the network in already running, we need to transition the neurons to their new positions
relattive to the new center */
this.instantTransition();
}
}
}
});
}
saveOriginalPositions() {
// saves the original positions of the neurons
for (let neuron of this.neurons) {
neuron.originalPosX = neuron.posX;
neuron.originalPosY = neuron.posY;
}
}
drawStaticNetwork() {
// draws the network without any transitions
this.drawInitialConnections();
this.drawInitialNeurons();
}
}
export class TransitionNetwork extends BasePainter {
constructor(htmlElement) {
super(htmlElement);
this.transitionTime = 500;
this.transitionInterval = 2000; // the time between the transitions
this.propertiesUpdater = (neurons, connections, iter) => { }; // gets called to set next transition positions
}
transitionNeurons() {
// transitions the neurons to their new positions
d3.select(this.svgElement)
.selectAll("circle")
.data(this.neurons, (el) => {
return el.index;
// return the index of the neuron
})
.transition()
.duration(this.transitionTime)
.attr("cx", (el) => {
return el.posX + this.center.x;
})
.attr("cy", (el) => {
return el.posY + this.center.y;
})
.attr("r", (el) => {
return el.radius;
})
.attr("fill", (el) => {
return el.bgColor;
})
.attr("stroke", (el) => {
return el.strokeColor;
})
.attr("stroke-width", (el) => {
return el.strokeWidth;
});
}
transitionConnections() {
// transitions the connections to their new positions
d3.select(this.svgElement)
.selectAll("line")
.data(this.connections, (el) => {
return el.index;
// return the index of the connection
})
.transition()
.duration(this.transitionTime)
.attr("x1", (el) => {
return this.neurons[el.idxNeuron1].posX + this.center.x;
})
.attr("y1", (el) => {
return this.neurons[el.idxNeuron1].posY + this.center.y;
})
.attr("x2", (el) => {
return this.neurons[el.idxNeuron2].posX + this.center.x;
})
.attr("y2", (el) => {
return this.neurons[el.idxNeuron2].posY + this.center.y;
})
.attr("stroke", (el) => {
return el.strokeColor;
})
.attr("stroke-width", (el) => {
return el.strokeWidth;
})
.attr("stroke-opacity", (el) => {
return el.strokeOpacity;
});
}
paramsTransitionChecker(params) {
if (params.infinite == false && params.iterations == undefined) {
throw "You need to specify the number of iterations or set infinite to true";
}
if (params.transitionTime != undefined) {
this.transitionTime = params.transitionTime;
}
else {
throw "You need to specify the transition time in ms";
}
if (params.transitionInterval != undefined) {
this.transitionInterval = params.transitionInterval;
}
else {
throw "You need to specify the transition interval in ms";
}
if (params.propertiesUpdater != undefined) {
this.propertiesUpdater = params.propertiesUpdater;
}
else {
throw "You need to specify the properties updater function";
}
} // checks if the params are valid and sets them to class
startRendering(params) {
return __awaiter(this, void 0, void 0, function* () {
// draws the initial neurons applying the properties
this.paramsTransitionChecker(params);
this.calculateSVGSizes();
this.checkSvgSize();
this.drawInitialConnections();
this.drawInitialNeurons();
// saving the original positions
this.saveOriginalPositions();
this.running = true;
// starts the render loop
this.iter = 0;
while (params.infinite ||
(params.iterations !== undefined && this.iter < params.iterations)) {
//the new positions will be calculated
if (this.hidden) {
yield delay(200);
continue;
}
this.propertiesUpdater(this.neurons, this.connections, this.iter);
// applies the new positions
this.applyPositions();
// sleep for transitionInterval
yield delay(this.transitionInterval);
// in the transition network the neurons will be moved to their new positions
this.transitionNeurons();
this.transitionConnections();
this.iter += 1;
}
this.running = false;
// write code for drawing the neurons
});
}
saveOriginalPositions() {
// saves the original positions of the neurons
for (let neuron of this.neurons) {
neuron.originalPosX = neuron.posX;
neuron.originalPosY = neuron.posY;
}
}
}
// the program will have a physics based implementation and an transition based implementation
export class PhysicsNetwork extends BasePainter {
constructor(htmlElement) {
super(htmlElement);
this.FPS = 60;
this.forceLoss = 0; // the force loss per frame
this.forceMultiplier = 0.005; // the force multiplier
this.forces = []; // the forces that will be applied to the neurons
this.addForces = (neurons, forces, iter) => { }; // gets called to add forces to the neurons
this.addInitialForces = (neurons, forces, iter) => { }; // gets called to add initial forces to the neurons
}
// movement will have no transition, positions will be calculated in the render loop
applyForces() {
for (let idx = 0; idx < this.neurons.length; idx++) {
const neuron = this.neurons[idx];
const force = this.forces[idx];
// the force will be applied to the neuron
neuron.newPosX = neuron.posX + force.x * this.forceMultiplier;
neuron.newPosY = neuron.posY + force.y * this.forceMultiplier;
// the force will be reduced
force.x *= 1 - this.forceLoss;
force.y *= 1 - this.forceLoss;
}
}
initializeForces() {
for (let idx = 0; idx < this.neurons.length; idx++) {
this.forces[idx] = { x: 0, y: 0 };
}
}
paramsPhysicsChecker(params) {
if (params.infinite == false && params.seconds == undefined) {
throw "You need to specify the number of seconds or set infinite to true";
}
if (params.addForces != undefined) {
this.addForces = params.addForces;
}
else {
throw "You need to specify the addForces function";
}
if (params.addInitialForces != undefined) {
this.addInitialForces = params.addInitialForces;
}
else {
this.addInitialForces = (neurons, forces, iter) => { };
}
if (params.FPS != undefined) {
this.FPS = params.FPS;
}
else {
throw "You need to specify the FPS";
}
if (params.forceLoss != undefined) {
this.forceLoss = params.forceLoss;
}
if (params.forceMultiplier != undefined) {
this.forceMultiplier = params.forceMultiplier;
}
}
// render loop
startRendering(params) {
return __awaiter(this, void 0, void 0, function* () {
// draws the initial neurons applying the properties
this.paramsPhysicsChecker(params);
this.calculateSVGSizes();
this.checkSvgSize();
this.drawInitialConnections();
this.drawInitialNeurons();
//intialization of the positionsDx and positionsDy arrays with 0 for each neuron
// saving the original positions
this.saveOriginalPositions();
this.running = true;
this.initializeForces();
// dispatch initial forces (may also pe async for loops)
this.addInitialForces(this.neurons, this.forces, this.iter);
// starts the render loop
while (params.infinite ||
(params.seconds !== undefined && this.iter < params.seconds * this.FPS)) {
if (this.hidden) {
yield delay(100);
continue;
}
// drawing the neurons with new positions
this.instantTransition();
// forces are added on Neurons
this.addForces(this.neurons, this.forces, this.iter);
// forces are applied, calculates dx and dy
this.applyForces();
// the new positions are calculated from the old positions and the dx and dy
this.applyPositions();
yield delay(1000 / this.FPS);
if (this.hidden) {
yield delay(100);
continue;
}
this.instantTransition();
}
this.running = false;
// write code for drawing the neurons
});
}
}