UNPKG

neuralaesthetics

Version:

Awesome neural networks for aesthetic web pages

598 lines (597 loc) 23.4 kB
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 }); } }