481 lines
14 KiB
JavaScript
481 lines
14 KiB
JavaScript
|
|
import graphicGL from '../../util/graphicGL';
|
||
|
|
import Pass from 'claygl/src/compositor/Pass';
|
||
|
|
import FrameBuffer from 'claygl/src/FrameBuffer';
|
||
|
|
import forceAtlas2Code from './forceAtlas2.glsl.js';
|
||
|
|
graphicGL.Shader.import(forceAtlas2Code);
|
||
|
|
var defaultConfigs = {
|
||
|
|
repulsionByDegree: true,
|
||
|
|
linLogMode: false,
|
||
|
|
strongGravityMode: false,
|
||
|
|
gravity: 1.0,
|
||
|
|
scaling: 1.0,
|
||
|
|
edgeWeightInfluence: 1.0,
|
||
|
|
jitterTolerence: 0.1,
|
||
|
|
preventOverlap: false,
|
||
|
|
dissuadeHubs: false,
|
||
|
|
gravityCenter: null
|
||
|
|
};
|
||
|
|
|
||
|
|
function ForceAtlas2GPU(options) {
|
||
|
|
var textureOpt = {
|
||
|
|
type: graphicGL.Texture.FLOAT,
|
||
|
|
minFilter: graphicGL.Texture.NEAREST,
|
||
|
|
magFilter: graphicGL.Texture.NEAREST
|
||
|
|
};
|
||
|
|
this._positionSourceTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._positionSourceTex.flipY = false;
|
||
|
|
this._positionTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._positionPrevTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._forceTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._forcePrevTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._weightedSumTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._weightedSumTex.width = this._weightedSumTex.height = 1;
|
||
|
|
this._globalSpeedTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._globalSpeedPrevTex = new graphicGL.Texture2D(textureOpt);
|
||
|
|
this._globalSpeedTex.width = this._globalSpeedTex.height = 1;
|
||
|
|
this._globalSpeedPrevTex.width = this._globalSpeedPrevTex.height = 1;
|
||
|
|
this._nodeRepulsionPass = new Pass({
|
||
|
|
fragment: graphicGL.Shader.source('ecgl.forceAtlas2.updateNodeRepulsion')
|
||
|
|
});
|
||
|
|
this._positionPass = new Pass({
|
||
|
|
fragment: graphicGL.Shader.source('ecgl.forceAtlas2.updatePosition')
|
||
|
|
});
|
||
|
|
this._globalSpeedPass = new Pass({
|
||
|
|
fragment: graphicGL.Shader.source('ecgl.forceAtlas2.calcGlobalSpeed')
|
||
|
|
});
|
||
|
|
this._copyPass = new Pass({
|
||
|
|
fragment: graphicGL.Shader.source('clay.compositor.output')
|
||
|
|
});
|
||
|
|
|
||
|
|
var additiveBlend = function (gl) {
|
||
|
|
gl.blendEquation(gl.FUNC_ADD);
|
||
|
|
gl.blendFunc(gl.ONE, gl.ONE);
|
||
|
|
};
|
||
|
|
|
||
|
|
this._edgeForceMesh = new graphicGL.Mesh({
|
||
|
|
geometry: new graphicGL.Geometry({
|
||
|
|
attributes: {
|
||
|
|
node1: new graphicGL.Geometry.Attribute('node1', 'float', 2),
|
||
|
|
node2: new graphicGL.Geometry.Attribute('node2', 'float', 2),
|
||
|
|
weight: new graphicGL.Geometry.Attribute('weight', 'float', 1)
|
||
|
|
},
|
||
|
|
dynamic: true,
|
||
|
|
mainAttribute: 'node1'
|
||
|
|
}),
|
||
|
|
material: new graphicGL.Material({
|
||
|
|
transparent: true,
|
||
|
|
shader: graphicGL.createShader('ecgl.forceAtlas2.updateEdgeAttraction'),
|
||
|
|
blend: additiveBlend,
|
||
|
|
depthMask: false,
|
||
|
|
depthText: false
|
||
|
|
}),
|
||
|
|
mode: graphicGL.Mesh.POINTS
|
||
|
|
});
|
||
|
|
this._weightedSumMesh = new graphicGL.Mesh({
|
||
|
|
geometry: new graphicGL.Geometry({
|
||
|
|
attributes: {
|
||
|
|
node: new graphicGL.Geometry.Attribute('node', 'float', 2)
|
||
|
|
},
|
||
|
|
dynamic: true,
|
||
|
|
mainAttribute: 'node'
|
||
|
|
}),
|
||
|
|
material: new graphicGL.Material({
|
||
|
|
transparent: true,
|
||
|
|
shader: graphicGL.createShader('ecgl.forceAtlas2.calcWeightedSum'),
|
||
|
|
blend: additiveBlend,
|
||
|
|
depthMask: false,
|
||
|
|
depthText: false
|
||
|
|
}),
|
||
|
|
mode: graphicGL.Mesh.POINTS
|
||
|
|
});
|
||
|
|
this._framebuffer = new FrameBuffer({
|
||
|
|
depthBuffer: false
|
||
|
|
});
|
||
|
|
this._dummyCamera = new graphicGL.OrthographicCamera({
|
||
|
|
left: -1,
|
||
|
|
right: 1,
|
||
|
|
top: 1,
|
||
|
|
bottom: -1,
|
||
|
|
near: 0,
|
||
|
|
far: 100
|
||
|
|
});
|
||
|
|
this._globalSpeed = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.updateOption = function (options) {
|
||
|
|
// Default config
|
||
|
|
for (var name in defaultConfigs) {
|
||
|
|
this[name] = defaultConfigs[name];
|
||
|
|
} // Config according to data scale
|
||
|
|
|
||
|
|
|
||
|
|
var nNodes = this._nodes.length;
|
||
|
|
|
||
|
|
if (nNodes > 50000) {
|
||
|
|
this.jitterTolerence = 10;
|
||
|
|
} else if (nNodes > 5000) {
|
||
|
|
this.jitterTolerence = 1;
|
||
|
|
} else {
|
||
|
|
this.jitterTolerence = 0.1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nNodes > 100) {
|
||
|
|
this.scaling = 2.0;
|
||
|
|
} else {
|
||
|
|
this.scaling = 10.0;
|
||
|
|
} // this.edgeWeightInfluence = 1;
|
||
|
|
// this.gravity = 1;
|
||
|
|
// this.strongGravityMode = false;
|
||
|
|
|
||
|
|
|
||
|
|
if (options) {
|
||
|
|
for (var name in defaultConfigs) {
|
||
|
|
if (options[name] != null) {
|
||
|
|
this[name] = options[name];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.repulsionByDegree) {
|
||
|
|
var positionBuffer = this._positionSourceTex.pixels;
|
||
|
|
|
||
|
|
for (var i = 0; i < this._nodes.length; i++) {
|
||
|
|
positionBuffer[i * 4 + 2] = (this._nodes[i].degree || 0) + 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype._updateGravityCenter = function (options) {
|
||
|
|
var nodes = this._nodes;
|
||
|
|
var edges = this._edges;
|
||
|
|
|
||
|
|
if (!this.gravityCenter) {
|
||
|
|
var min = [Infinity, Infinity];
|
||
|
|
var max = [-Infinity, -Infinity];
|
||
|
|
|
||
|
|
for (var i = 0; i < nodes.length; i++) {
|
||
|
|
min[0] = Math.min(nodes[i].x, min[0]);
|
||
|
|
min[1] = Math.min(nodes[i].y, min[1]);
|
||
|
|
max[0] = Math.max(nodes[i].x, max[0]);
|
||
|
|
max[1] = Math.max(nodes[i].y, max[1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._gravityCenter = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5];
|
||
|
|
} else {
|
||
|
|
this._gravityCenter = this.gravityCenter;
|
||
|
|
} // Update inDegree, outDegree
|
||
|
|
|
||
|
|
|
||
|
|
for (var i = 0; i < edges.length; i++) {
|
||
|
|
var node1 = edges[i].node1;
|
||
|
|
var node2 = edges[i].node2;
|
||
|
|
nodes[node1].degree = (nodes[node1].degree || 0) + 1;
|
||
|
|
nodes[node2].degree = (nodes[node2].degree || 0) + 1;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* @param {Array.<Object>} [{ x, y, mass }] nodes
|
||
|
|
* @param {Array.<Object>} [{ node1, node2, weight }] edges
|
||
|
|
*/
|
||
|
|
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.initData = function (nodes, edges) {
|
||
|
|
this._nodes = nodes;
|
||
|
|
this._edges = edges;
|
||
|
|
|
||
|
|
this._updateGravityCenter();
|
||
|
|
|
||
|
|
var textureWidth = Math.ceil(Math.sqrt(nodes.length));
|
||
|
|
var textureHeight = textureWidth;
|
||
|
|
var positionBuffer = new Float32Array(textureWidth * textureHeight * 4);
|
||
|
|
|
||
|
|
this._resize(textureWidth, textureHeight);
|
||
|
|
|
||
|
|
var offset = 0;
|
||
|
|
|
||
|
|
for (var i = 0; i < nodes.length; i++) {
|
||
|
|
var node = nodes[i];
|
||
|
|
positionBuffer[offset++] = node.x || 0;
|
||
|
|
positionBuffer[offset++] = node.y || 0;
|
||
|
|
positionBuffer[offset++] = node.mass || 1;
|
||
|
|
positionBuffer[offset++] = node.size || 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._positionSourceTex.pixels = positionBuffer;
|
||
|
|
var edgeGeometry = this._edgeForceMesh.geometry;
|
||
|
|
var edgeLen = edges.length;
|
||
|
|
edgeGeometry.attributes.node1.init(edgeLen * 2);
|
||
|
|
edgeGeometry.attributes.node2.init(edgeLen * 2);
|
||
|
|
edgeGeometry.attributes.weight.init(edgeLen * 2);
|
||
|
|
var uv = [];
|
||
|
|
|
||
|
|
for (var i = 0; i < edges.length; i++) {
|
||
|
|
var attributes = edgeGeometry.attributes;
|
||
|
|
var weight = edges[i].weight;
|
||
|
|
|
||
|
|
if (weight == null) {
|
||
|
|
weight = 1;
|
||
|
|
} // Two way.
|
||
|
|
|
||
|
|
|
||
|
|
attributes.node1.set(i, this.getNodeUV(edges[i].node1, uv));
|
||
|
|
attributes.node2.set(i, this.getNodeUV(edges[i].node2, uv));
|
||
|
|
attributes.weight.set(i, weight);
|
||
|
|
attributes.node1.set(i + edgeLen, this.getNodeUV(edges[i].node2, uv));
|
||
|
|
attributes.node2.set(i + edgeLen, this.getNodeUV(edges[i].node1, uv));
|
||
|
|
attributes.weight.set(i + edgeLen, weight);
|
||
|
|
}
|
||
|
|
|
||
|
|
var weigtedSumGeo = this._weightedSumMesh.geometry;
|
||
|
|
weigtedSumGeo.attributes.node.init(nodes.length);
|
||
|
|
|
||
|
|
for (var i = 0; i < nodes.length; i++) {
|
||
|
|
weigtedSumGeo.attributes.node.set(i, this.getNodeUV(i, uv));
|
||
|
|
}
|
||
|
|
|
||
|
|
edgeGeometry.dirty();
|
||
|
|
weigtedSumGeo.dirty();
|
||
|
|
|
||
|
|
this._nodeRepulsionPass.material.define('fragment', 'NODE_COUNT', nodes.length);
|
||
|
|
|
||
|
|
this._nodeRepulsionPass.material.setUniform('textureSize', [textureWidth, textureHeight]);
|
||
|
|
|
||
|
|
this._inited = false;
|
||
|
|
this._frame = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getNodes = function () {
|
||
|
|
return this._nodes;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getEdges = function () {
|
||
|
|
return this._edges;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.step = function (renderer) {
|
||
|
|
if (!this._inited) {
|
||
|
|
this._initFromSource(renderer);
|
||
|
|
|
||
|
|
this._inited = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._frame++;
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._forceTex);
|
||
|
|
|
||
|
|
this._framebuffer.bind(renderer);
|
||
|
|
|
||
|
|
var nodeRepulsionPass = this._nodeRepulsionPass; // Calc node repulsion, gravity
|
||
|
|
|
||
|
|
nodeRepulsionPass.setUniform('strongGravityMode', this.strongGravityMode);
|
||
|
|
nodeRepulsionPass.setUniform('gravity', this.gravity);
|
||
|
|
nodeRepulsionPass.setUniform('gravityCenter', this._gravityCenter);
|
||
|
|
nodeRepulsionPass.setUniform('scaling', this.scaling);
|
||
|
|
nodeRepulsionPass.setUniform('preventOverlap', this.preventOverlap);
|
||
|
|
nodeRepulsionPass.setUniform('positionTex', this._positionPrevTex);
|
||
|
|
nodeRepulsionPass.render(renderer); // Calc edge attraction force
|
||
|
|
|
||
|
|
var edgeForceMesh = this._edgeForceMesh;
|
||
|
|
edgeForceMesh.material.set('linLogMode', this.linLogMode);
|
||
|
|
edgeForceMesh.material.set('edgeWeightInfluence', this.edgeWeightInfluence);
|
||
|
|
edgeForceMesh.material.set('preventOverlap', this.preventOverlap);
|
||
|
|
edgeForceMesh.material.set('positionTex', this._positionPrevTex);
|
||
|
|
renderer.gl.enable(renderer.gl.BLEND);
|
||
|
|
renderer.renderPass([edgeForceMesh], this._dummyCamera); // Calc weighted sum.
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._weightedSumTex);
|
||
|
|
|
||
|
|
renderer.gl.clearColor(0, 0, 0, 0);
|
||
|
|
renderer.gl.clear(renderer.gl.COLOR_BUFFER_BIT);
|
||
|
|
renderer.gl.enable(renderer.gl.BLEND);
|
||
|
|
var weightedSumMesh = this._weightedSumMesh;
|
||
|
|
weightedSumMesh.material.set('positionTex', this._positionPrevTex);
|
||
|
|
weightedSumMesh.material.set('forceTex', this._forceTex);
|
||
|
|
weightedSumMesh.material.set('forcePrevTex', this._forcePrevTex);
|
||
|
|
renderer.renderPass([weightedSumMesh], this._dummyCamera); // Calc global speed.
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._globalSpeedTex);
|
||
|
|
|
||
|
|
var globalSpeedPass = this._globalSpeedPass;
|
||
|
|
globalSpeedPass.setUniform('globalSpeedPrevTex', this._globalSpeedPrevTex);
|
||
|
|
globalSpeedPass.setUniform('weightedSumTex', this._weightedSumTex);
|
||
|
|
globalSpeedPass.setUniform('jitterTolerence', this.jitterTolerence);
|
||
|
|
renderer.gl.disable(renderer.gl.BLEND);
|
||
|
|
globalSpeedPass.render(renderer); // Update position.
|
||
|
|
|
||
|
|
var positionPass = this._positionPass;
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._positionTex);
|
||
|
|
|
||
|
|
positionPass.setUniform('globalSpeedTex', this._globalSpeedTex);
|
||
|
|
positionPass.setUniform('positionTex', this._positionPrevTex);
|
||
|
|
positionPass.setUniform('forceTex', this._forceTex);
|
||
|
|
positionPass.setUniform('forcePrevTex', this._forcePrevTex);
|
||
|
|
positionPass.render(renderer);
|
||
|
|
|
||
|
|
this._framebuffer.unbind(renderer);
|
||
|
|
|
||
|
|
this._swapTexture();
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.update = function (renderer, steps, cb) {
|
||
|
|
if (steps == null) {
|
||
|
|
steps = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
steps = Math.max(steps, 1);
|
||
|
|
|
||
|
|
for (var i = 0; i < steps; i++) {
|
||
|
|
this.step(renderer);
|
||
|
|
}
|
||
|
|
|
||
|
|
cb && cb();
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getNodePositionTexture = function () {
|
||
|
|
return this._inited // Texture already been swapped.
|
||
|
|
? this._positionPrevTex : this._positionSourceTex;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getNodeUV = function (nodeIndex, uv) {
|
||
|
|
uv = uv || [];
|
||
|
|
var textureWidth = this._positionTex.width;
|
||
|
|
var textureHeight = this._positionTex.height;
|
||
|
|
uv[0] = nodeIndex % textureWidth / (textureWidth - 1);
|
||
|
|
uv[1] = Math.floor(nodeIndex / textureWidth) / (textureHeight - 1) || 0;
|
||
|
|
return uv;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getNodePosition = function (renderer, out) {
|
||
|
|
var positionArr = this._positionArr;
|
||
|
|
var width = this._positionTex.width;
|
||
|
|
var height = this._positionTex.height;
|
||
|
|
var size = width * height;
|
||
|
|
|
||
|
|
if (!positionArr || positionArr.length !== size * 4) {
|
||
|
|
positionArr = this._positionArr = new Float32Array(size * 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._framebuffer.bind(renderer);
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._positionPrevTex);
|
||
|
|
|
||
|
|
renderer.gl.readPixels(0, 0, width, height, renderer.gl.RGBA, renderer.gl.FLOAT, positionArr);
|
||
|
|
|
||
|
|
this._framebuffer.unbind(renderer);
|
||
|
|
|
||
|
|
if (!out) {
|
||
|
|
out = new Float32Array(this._nodes.length * 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (var i = 0; i < this._nodes.length; i++) {
|
||
|
|
out[i * 2] = positionArr[i * 4];
|
||
|
|
out[i * 2 + 1] = positionArr[i * 4 + 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getTextureData = function (renderer, textureName) {
|
||
|
|
var tex = this['_' + textureName + 'Tex'];
|
||
|
|
var width = tex.width;
|
||
|
|
var height = tex.height;
|
||
|
|
|
||
|
|
this._framebuffer.bind(renderer);
|
||
|
|
|
||
|
|
this._framebuffer.attach(tex);
|
||
|
|
|
||
|
|
var arr = new Float32Array(width * height * 4);
|
||
|
|
renderer.gl.readPixels(0, 0, width, height, renderer.gl.RGBA, renderer.gl.FLOAT, arr);
|
||
|
|
|
||
|
|
this._framebuffer.unbind(renderer);
|
||
|
|
|
||
|
|
return arr;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.getTextureSize = function () {
|
||
|
|
return {
|
||
|
|
width: this._positionTex.width,
|
||
|
|
height: this._positionTex.height
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.isFinished = function (maxSteps) {
|
||
|
|
return this._frame > maxSteps;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype._swapTexture = function () {
|
||
|
|
var tmp = this._positionPrevTex;
|
||
|
|
this._positionPrevTex = this._positionTex;
|
||
|
|
this._positionTex = tmp;
|
||
|
|
var tmp = this._forcePrevTex;
|
||
|
|
this._forcePrevTex = this._forceTex;
|
||
|
|
this._forceTex = tmp;
|
||
|
|
var tmp = this._globalSpeedPrevTex;
|
||
|
|
this._globalSpeedPrevTex = this._globalSpeedTex;
|
||
|
|
this._globalSpeedTex = tmp;
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype._initFromSource = function (renderer) {
|
||
|
|
this._framebuffer.attach(this._positionPrevTex);
|
||
|
|
|
||
|
|
this._framebuffer.bind(renderer);
|
||
|
|
|
||
|
|
this._copyPass.setUniform('texture', this._positionSourceTex);
|
||
|
|
|
||
|
|
this._copyPass.render(renderer);
|
||
|
|
|
||
|
|
renderer.gl.clearColor(0, 0, 0, 0);
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._forcePrevTex);
|
||
|
|
|
||
|
|
renderer.gl.clear(renderer.gl.COLOR_BUFFER_BIT);
|
||
|
|
|
||
|
|
this._framebuffer.attach(this._globalSpeedPrevTex);
|
||
|
|
|
||
|
|
renderer.gl.clear(renderer.gl.COLOR_BUFFER_BIT);
|
||
|
|
|
||
|
|
this._framebuffer.unbind(renderer);
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype._resize = function (width, height) {
|
||
|
|
['_positionSourceTex', '_positionTex', '_positionPrevTex', '_forceTex', '_forcePrevTex'].forEach(function (texName) {
|
||
|
|
this[texName].width = width;
|
||
|
|
this[texName].height = height;
|
||
|
|
this[texName].dirty();
|
||
|
|
}, this);
|
||
|
|
};
|
||
|
|
|
||
|
|
ForceAtlas2GPU.prototype.dispose = function (renderer) {
|
||
|
|
this._framebuffer.dispose(renderer);
|
||
|
|
|
||
|
|
this._copyPass.dispose(renderer);
|
||
|
|
|
||
|
|
this._nodeRepulsionPass.dispose(renderer);
|
||
|
|
|
||
|
|
this._positionPass.dispose(renderer);
|
||
|
|
|
||
|
|
this._globalSpeedPass.dispose(renderer);
|
||
|
|
|
||
|
|
this._edgeForceMesh.geometry.dispose(renderer);
|
||
|
|
|
||
|
|
this._weightedSumMesh.geometry.dispose(renderer);
|
||
|
|
|
||
|
|
this._positionSourceTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._positionTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._positionPrevTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._forceTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._forcePrevTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._weightedSumTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._globalSpeedTex.dispose(renderer);
|
||
|
|
|
||
|
|
this._globalSpeedPrevTex.dispose(renderer);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default ForceAtlas2GPU;
|