UNPKG

@jonobr1/force-directed-graph

Version:

GPU supercharged attraction-graph visualizations for the web built on top of Three.js

1,438 lines (1,309 loc) 38.8 kB
// src/index.js import { Color as Color3, Group, RepeatWrapping, Vector2, Vector3 } from "three"; import { GPUComputationRenderer } from "three/examples/jsm/misc/GPUComputationRenderer.js"; // src/math.js var pot = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; function getPotSize(number) { const side = Math.floor(Math.sqrt(number)) + 1; for (let i = 0; i < pot.length; i++) { if (pot[i] >= side) { return pot[i]; } } console.error( "ForceDirectedGraph: Texture size is too big.", "Consider reducing the size of your data." ); } function clamp(x, min, max) { return Math.min(Math.max(x, min), max); } var maxFrames = 1e3; function each(list, func, step, max) { if (typeof step !== "number") { step = 1; } if (typeof max !== "number") { max = maxFrames; } return new Promise((resolve) => { exec(0); function exec(start) { const limit = Math.min(start + maxFrames, list.length); let i = start; while (i < limit) { func(list[i], i); i += step; } if (limit < list.length) { requestAnimationFrame(() => exec(i)); } else { resolve(); } } }); } function rgbToIndex({ r, g, b }) { return r + g * 255 + b * Math.pow(255, 2); } // src/shaders/positions.js var positions = ` uniform float is2D; uniform float timeStep; void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; vec4 texel = texture2D( texturePositions, uv ); vec3 position = texel.xyz; vec3 velocity = texture2D( textureVelocities, uv ).xyz; float isStatic = texel.w; vec3 result = position + velocity * timeStep * ( 1.0 - isStatic ); gl_FragColor = vec4( result.xyz, isStatic ); } `; // src/shaders/partials.js var circle = ` float circle( vec2 uv, vec2 pos, float rad, float isSmooth ) { float limit = 0.02; float limit2 = limit * 2.0; float d = length( pos - uv ) - ( rad - limit ); float t = clamp( d, 0.0, 1.0 ); float viewRange = smoothstep( 0.0, frustumSize * 0.001, abs( vDistance ) ); float taper = limit2 * viewRange + limit; taper = mix( taper, limit2, sizeAttenuation ); float a = step( 0.5, 1.0 - t ); float aa = smoothstep( 0.5 - taper, 0.5 + taper, 1.0 - t );; return mix( a, aa, isSmooth ); } `; var getPosition = ` vec3 getPosition( vec2 uv ) { return texture2D( texturePositions, uv ).xyz; } `; var getVelocity = ` vec3 getVelocity( vec2 uv ) { return texture2D( textureVelocities, uv ).xyz; } `; var getIndex = ` int getIndex( vec2 uv ) { int s = int( size ); int col = int( uv.x * size ); int row = int( uv.y * size ); return col + row * s; } `; var random = ` float random( vec2 seed ) { return fract( sin( dot( seed.xy, vec2( 12.9898, 78.233 ) ) ) * 43758.5453 ); } `; var jiggle = ` float jiggle( float index ) { return ( random( vec2( index, time ) ) - 0.5 ) * 0.000001; } `; var link = ` vec3 link( float i, int id1, vec3 p1, vec3 v1, vec2 uv2 ) { vec3 result = vec3( 0.0 ); vec4 edge = texture2D( textureLinks, uv2 ); vec2 source = edge.xy; vec2 target = edge.zw; int si = getIndex( source ); float siF = float( si ); vec3 sv = getVelocity( source ); vec3 sp = getPosition( source ); int ti = getIndex( target ); float tiF = float( ti ); vec3 tv = getVelocity( target ); vec3 tp = getPosition( target ); vec3 diff = tp + tv - ( sp + sv ); diff.z *= 1.0 - is2D; vec3 mag = abs( diff ); float seed = float( si + ti ); float bias = 0.5; float dist = length( diff ); dist = stiffness * ( dist - springLength ) / dist; diff *= dist; if ( id1 == ti ) { result -= diff * bias; } else if ( id1 == si ) { result += diff * bias; } result.z *= 1.0 - is2D; return result; } `; var charge = ` vec3 charge( float i, int id1, vec3 p1, vec3 v1, int id2, vec3 v2, vec3 p2 ) { vec3 result = vec3( 0.0 ); vec3 diff = ( p2 + v2 ) - ( p1 + v1 ); diff.z *= 1.0 - is2D; float dist = length( diff ); float mag = repulsion / dist; vec3 dir = normalize( diff ); if ( id1 != id2 ) { result += dir * mag; } result.z *= 1.0 - is2D; return result; } `; var center = ` vec3 center( vec3 p1 ) { return - p1 * gravity * 0.1; } `; // src/shaders/velocities.js var types = ["simplex", "nested"]; var simplex = ` uniform float alpha; uniform float is2D; uniform float size; uniform float time; uniform float nodeRadius; uniform float nodeAmount; uniform float edgeAmount; uniform float maxSpeed; uniform float timeStep; uniform float damping; uniform float repulsion; uniform float springLength; uniform float stiffness; uniform float gravity; uniform sampler2D textureLinks; ${getPosition} ${getVelocity} ${getIndex} ${random} ${jiggle} ${link} ${charge} ${center} void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; int id1 = getIndex( uv ); vec3 p1 = getPosition( uv ); vec3 v1 = getVelocity( uv ); vec3 a = vec3( 0.0 ), b = vec3( 0.0 ), c = vec3( 0.0 ); for ( float i = 0.0; i < max( nodeAmount, edgeAmount ); i += 1.0 ) { float uvx = mod( i, size ) / size; float uvy = floor( i / size ) / size; vec2 uv2 = vec2( uvx, uvy ); int id2 = getIndex( uv2 ); vec3 v2 = getVelocity( uv2 ); vec3 p2 = getPosition( uv2 ); if ( i < edgeAmount ) { b += link( i, id1, p1, v1, uv2 ); } if ( i < nodeAmount) { c += charge( i, id1, p1, v1, id2, p2, v2 ); } } b *= 1.0 - step( edgeAmount, float( id1 ) ); c *= 1.0 - step( nodeAmount, float( id1 ) ); // 4. vec3 d = center( p1 ); vec3 acceleration = a + b + c + d; // Calculate Velocity vec3 velocity = ( v1 + ( acceleration * timeStep ) ) * damping * alpha; velocity = clamp( velocity, - maxSpeed, maxSpeed ); velocity.z *= 1.0 - is2D; gl_FragColor = vec4( velocity, 0.0 ); } `; var nested = ` uniform float alpha; uniform float is2D; uniform float size; uniform float time; uniform float nodeRadius; uniform float nodeAmount; uniform float edgeAmount; uniform float maxSpeed; uniform float timeStep; uniform float damping; uniform float repulsion; uniform float springLength; uniform float stiffness; uniform float gravity; uniform sampler2D textureLinks; uniform sampler2D textureLinksLookUp; ${getPosition} ${getVelocity} ${getIndex} ${random} ${jiggle} ${link} ${charge} ${center} void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; int id1 = getIndex( uv ); vec3 p1 = getPosition( uv ); vec3 v1 = getVelocity( uv ); vec3 a = vec3( 0.0 ), b = vec3( 0.0 ), c = vec3( 0.0 ); /* for ( float i = 0.0; i < linkAmount; i += 1.0 ) { // TODO: get all edges and link them b += link( i, id1, p1, v1, uv2 ); } */ for ( float i = 0.0; i < nodeAmount; i += 1.0 ) { float uvx = mod( i, size ) / size; float uvy = floor( i / size ) / size; vec2 uv2 = vec2( uvx, uvy ); int id2 = getIndex( uv2 ); vec3 v2 = getVelocity( uv2 ); vec3 p2 = getPosition( uv2 ); if ( i < nodeAmount) { c += charge( i, id1, p1, v1, id2, p2, v2 ); } } b *= 1.0 - step( edgeAmount, float( id1 ) ); c *= 1.0 - step( nodeAmount, float( id1 ) ); // 4. vec3 d = center( p1 ); vec3 acceleration = a + b + c + d; // Calculate Velocity vec3 velocity = ( v1 + ( acceleration * timeStep ) ) * damping * alpha; velocity = clamp( velocity, - maxSpeed, maxSpeed ); velocity.z *= 1.0 - is2D; gl_FragColor = vec4( velocity, 0.0 ); } `; // src/shaders/simulation.js var simulation_default = { positions, velocities: simplex, simplex, nested, types }; // src/points.js import { Points as BasePoints, BufferGeometry, Float32BufferAttribute, ShaderMaterial, Color, UniformsLib } from "three"; // src/shaders/points.js var points = { vertexShader: ` #include <fog_pars_vertex> uniform float sizeAttenuation; uniform float frustumSize; uniform float is2D; uniform float nodeRadius; uniform float nodeScale; uniform sampler2D texturePositions; varying vec3 vColor; varying float vImageKey; varying float vDistance; attribute float imageKey; void main() { vec4 texel = texture2D( texturePositions, position.xy ); vec3 vPosition = texel.xyz; vPosition.z *= 1.0 - is2D; vec4 mvPosition = modelViewMatrix * vec4( vPosition, 1.0 ); gl_PointSize = nodeRadius * nodeScale; gl_PointSize *= mix( 1.0, frustumSize / - mvPosition.z, sizeAttenuation ); vDistance = 1.0 / - mvPosition.z; vColor = color; vImageKey = imageKey; gl_Position = projectionMatrix * mvPosition; #include <fog_vertex> } `, fragmentShader: ` #include <fog_pars_fragment> uniform float sizeAttenuation; uniform float frustumSize; uniform vec3 uColor; uniform float opacity; uniform float imageDimensions; uniform sampler2D textureAtlas; uniform float inheritColors; varying vec3 vColor; varying float vImageKey; varying float vDistance; ${circle} void main() { vec2 uv = 2.0 * vec2( gl_PointCoord ) - 1.0; float t = circle( uv, vec2( 0.0, 0.0 ), 0.5, 1.0 ); float col = mod( vImageKey, imageDimensions ); float row = floor( vImageKey / imageDimensions ); uv = vec2( 0.0 ); uv.x = mix( 0.0, 1.0 / imageDimensions, gl_PointCoord.x ); uv.y = mix( 0.0, 1.0 / imageDimensions, gl_PointCoord.y ); uv = vec2( gl_PointCoord ) / imageDimensions; uv.x += col / imageDimensions; uv.y += row / imageDimensions; vec4 texel = texture2D( textureAtlas, uv ); float useImage = step( 0.0, vImageKey ); t = mix( t, texel.a, useImage ); vec3 layer = mix( vec3( 1.0 ), texel.rgb, useImage ); float alpha = opacity * t; if ( alpha <= 0.0 ) { discard; } gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha ); #include <fog_fragment> } ` }; var points_default = points; // src/texture-atlas.js import { Texture } from "three"; var anchor; var TextureAtlas = class _TextureAtlas extends Texture { map = []; dimensions = 1; isTextureAtlas = true; constructor() { if (!anchor) { anchor = document.createElement("a"); } super(document.createElement("canvas")); this.flipY = false; } static Resolution = 1024; static getAbsoluteURL(path) { anchor.href = path; return anchor.href; } add(src) { const scope = this; let img, index; if (typeof src === "string") { index = this.indexOf(src); if (index >= 0) { img = this.map[index]; if (img.complete) { onLoad(); } else { img.addEventListener("load", onLoad, false); } } else { img = document.createElement("img"); img.addEventListener("load", onLoad, false); img.src = src; index = this.map.length; this.map.push(img); } } else if (typeof src === "object" && "src" in src) { img = src; src = img.src; index = this.indexOf(src); if (index >= 0) { img = this.map[index]; } else { index = this.map.length; this.map.push(img); } if (img.complete) { onLoad(); } else { img.addEventListener("load", onLoad, false); } } this.dimensions = Math.ceil(Math.sqrt(this.map.length)); return index; function onLoad() { img.removeEventListener("load", onLoad, false); scope.update(); } } update() { const { image } = this; const ctx = image.getContext("2d"); image.width = _TextureAtlas.Resolution; image.height = _TextureAtlas.Resolution; const dims = this.dimensions = Math.ceil(Math.sqrt(this.map.length)); const width = image.width / dims; const height = image.height / dims; ctx.clearRect(0, 0, image.width, image.height); for (let i = 0; i < this.map.length; i++) { const col = i % dims; const row = Math.floor(i / dims); const img = this.map[i]; const x = col / dims * image.width; const y = row / dims * image.height; ctx.drawImage(img, x, y, width, height); } this.needsUpdate = true; } indexOf(src) { const uri = _TextureAtlas.getAbsoluteURL(src); for (let i = 0; i < this.map.length; i++) { const img = this.map[i]; if (uri === img.src) { return i; } } return -1; } }; // src/points.js var color = new Color(); var Points = class extends BasePoints { constructor({ atlas, geometry }, uniforms) { const material = new ShaderMaterial({ uniforms: { ...UniformsLib["fog"], ...{ is2D: uniforms.is2D, sizeAttenuation: uniforms.sizeAttenuation, frustumSize: uniforms.frustumSize, nodeRadius: uniforms.nodeRadius, nodeScale: uniforms.nodeScale, imageDimensions: { value: atlas.dimensions }, texturePositions: { value: null }, textureAtlas: { value: atlas }, size: uniforms.size, opacity: uniforms.opacity, uColor: uniforms.pointColor, inheritColors: uniforms.pointsInheritColor } }, vertexShader: points_default.vertexShader, fragmentShader: points_default.fragmentShader, transparent: true, vertexColors: true, fog: true }); super(geometry, material); this.frustumCulled = false; } static parse(size2, data) { const atlas = new TextureAtlas(); const vertices = []; const colors = []; const imageKeys = []; return each(data.nodes, (_, i) => { const node = data.nodes[i]; const x = i % size2 / size2; const y = Math.floor(i / size2) / size2; const z = i + 1; vertices.push(x, y, z); if (node.color) { color.set(node.color); colors.push(color.r, color.g, color.b); } else { colors.push(1, 1, 1); } if (node.image) { imageKeys.push(atlas.add(node.image)); } else { imageKeys.push(-1); } }).then(() => { const geometry = new BufferGeometry(); geometry.setAttribute( "position", new Float32BufferAttribute(vertices, 3) ); geometry.setAttribute( "color", new Float32BufferAttribute(colors, 3) ); geometry.setAttribute( "imageKey", new Float32BufferAttribute(imageKeys, 1) ); return { atlas, geometry }; }); } }; // src/links.js import { LineSegments, BufferGeometry as BufferGeometry2, Float32BufferAttribute as Float32BufferAttribute2, ShaderMaterial as ShaderMaterial2, UniformsLib as UniformsLib2 } from "three"; // src/shaders/links.js var links = { vertexShader: ` #include <fog_pars_vertex> uniform float is2D; uniform sampler2D texturePositions; varying vec3 vColor; void main() { vec3 vPosition = texture2D( texturePositions, position.xy ).xyz; vPosition.z *= 1.0 - is2D; vec4 mvPosition = modelViewMatrix * vec4( vPosition, 1.0 ); vColor = color; gl_Position = projectionMatrix * mvPosition; #include <fog_vertex> } `, fragmentShader: ` #include <fog_pars_fragment> uniform float inheritColors; uniform vec3 uColor; uniform float opacity; varying vec3 vColor; void main() { gl_FragColor = vec4( mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, opacity ); #include <fog_fragment> } ` }; var links_default = links; // src/links.js var Links = class extends LineSegments { constructor(geometry, uniforms) { const material = new ShaderMaterial2({ uniforms: { ...UniformsLib2["fog"], ...{ is2D: uniforms.is2D, inheritColors: uniforms.linksInheritColor, opacity: uniforms.opacity, texturePositions: { value: null }, uColor: uniforms.linkColor } }, vertexShader: links_default.vertexShader, fragmentShader: links_default.fragmentShader, transparent: true, vertexColors: true, fog: true }); super(geometry, material); this.frustumCulled = false; } static parse(points2, data) { const geometry = new BufferGeometry2(); const vertices = []; const colors = []; const v = points2.geometry.attributes.position.array; const c = points2.geometry.attributes.color.array; return each(data.links, (_, i) => { const l = data.links[i]; const si = 3 * l.sourceIndex; const ti = 3 * l.targetIndex; let x = v[si + 0]; let y = v[si + 1]; let z = v[si + 2]; let r = c[si + 0]; let g = c[si + 1]; let b = c[si + 2]; vertices.push(x, y, z); colors.push(r, g, b); x = v[ti + 0]; y = v[ti + 1]; z = v[ti + 2]; r = c[ti + 0]; g = c[ti + 1]; b = c[ti + 2]; vertices.push(x, y, z); colors.push(r, g, b); }).then(() => { geometry.setAttribute( "position", new Float32BufferAttribute2(vertices, 3) ); geometry.setAttribute( "color", new Float32BufferAttribute2(colors, 3) ); return geometry; }); } }; // src/registry.js var Registry = class { map = {}; constructor(list) { if (list && list.length > 0) { for (let i = 0; i < list.length; i++) { this.set(i, list[i]); } } } get(id) { return this.map[id]; } set(index, item) { if (item.id !== "undefined") { this.map[item.id] = index; } } clear() { this.map = {}; } }; // src/hit.js import { Color as Color2, ShaderMaterial as ShaderMaterial3, WebGLRenderTarget, Sprite, SpriteMaterial } from "three"; // src/shaders/hit.js var hit = { vertexShader: ` uniform float sizeAttenuation; uniform float frustumSize; uniform float is2D; uniform float nodeRadius; uniform float nodeScale; uniform float hitScale; uniform sampler2D texturePositions; varying vec3 vColor; varying float vDistance; void main() { vec4 texel = texture2D( texturePositions, position.xy ); vec3 vPosition = texel.xyz; vPosition.z *= 1.0 - is2D; vec4 mvPosition = modelViewMatrix * vec4( vPosition, 1.0 ); gl_PointSize = nodeRadius * nodeScale; gl_PointSize *= mix( 1.0, frustumSize / - mvPosition.z, sizeAttenuation ); gl_PointSize *= hitScale; vDistance = 1.0 / - mvPosition.z; float r = mod( position.z, 255.0 ) / 255.0; float g = mod( floor( position.z / 255.0 ), 255.0 ) / 255.0; float b = mod( floor( position.z / pow( 255.0, 2.0 ) ), 255.0 ) / 255.0; vColor = vec3( r, g, b ); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` uniform float sizeAttenuation; uniform float frustumSize; varying vec3 vColor; varying float vDistance; ${circle} void main() { vec2 uv = 2.0 * vec2( gl_PointCoord ) - 1.0; float t = circle( uv, vec2( 0.0, 0.0 ), 0.5, 0.0 ); gl_FragColor = vec4( vColor, t ); } ` }; var hit_default = hit; // src/hit.js var color2 = new Color2(); var Hit = class { parent = null; renderTarget = new WebGLRenderTarget(1, 1); width = 1; height = 1; ratio = 1; material = null; helper = null; constructor(fdg) { this.parent = fdg; this.helper = new Sprite(new SpriteMaterial({ map: this.renderTarget.texture })); this.material = new ShaderMaterial3({ uniforms: { hitScale: { value: 2 } }, vertexShader: hit_default.vertexShader, fragmentShader: hit_default.fragmentShader, transparent: true }); } inherit(mesh) { this.material.uniforms = { ...this.material.uniforms, ...mesh.material.uniforms }; } setSize(width, height) { const { helper, ratio, renderTarget } = this; const w = width * ratio; const h = height * ratio; if (this.width !== width || this.height !== height) { this.width = width; this.height = height; renderTarget.setSize(w, h); helper.scale.set(w, h, 1); } } compute(renderer, camera) { const { parent } = this; const renderTarget = renderer.getRenderTarget(); renderer.setRenderTarget(this.renderTarget); const material = parent.points.material; const visible = parent.links.visible; const alpha = renderer.getClearAlpha(); renderer.getClearColor(color2); parent.points.material = this.material; parent.links.visible = false; renderer.setClearColor(0, 0); renderer.render(parent, camera); parent.points.material = material; parent.links.visible = visible; renderer.setRenderTarget(renderTarget); renderer.setClearColor(color2, alpha); } dispose() { this.parent = null; this.renderTarget = new WebGLRenderTarget(1, 1); this.width = 1; this.height = 1; this.ratio = 1; this.material.dispose(); this.helper.geometry.dispose(); this.helper.material.dispose(); this.material = null; this.helper = null; } }; // src/index.js var color3 = new Color3(); var position = new Vector3(); var size = new Vector2(); var buffers = { int: new Uint8ClampedArray(4), float: new Float32Array(4) }; var ForceDirectedGraph = class extends Group { ready = false; /** * @param {THREE.WebGLRenderer} renderer - the three.js renderer referenced to create the render targets * @param {Object} [data] - optional data to automatically set the data of the graph */ constructor(renderer, data) { super(); this.userData.registry = new Registry(); this.userData.renderer = renderer; this.userData.uniforms = { decay: { value: 1 }, alpha: { value: 1 }, is2D: { value: false }, time: { value: 0 }, size: { value: 64 }, maxSpeed: { value: 10 }, timeStep: { value: 1 }, damping: { value: 0.7 }, repulsion: { value: -0.3 }, springLength: { value: 2 }, stiffness: { value: 0.1 }, gravity: { value: 0.1 }, nodeRadius: { value: 1 }, nodeScale: { value: 8 }, sizeAttenuation: { value: true }, frustumSize: { value: 100 }, linksInheritColor: { value: false }, pointsInheritColor: { value: true }, pointColor: { value: new Color3(1, 1, 1) }, linkColor: { value: new Color3(1, 1, 1) }, opacity: { value: 1 } }; this.userData.hit = new Hit(this); if (data) { this.set(data); } } static getPotSize = getPotSize; static Properties = [ "decay", "alpha", "is2D", "time", "size", "maxSpeed", "timeStep", "damping", "repulsion", "springLength", "stiffness", "gravity", "nodeRadius", "nodeScale", "sizeAttenuation", "frustumSize", "linksInheritColor", "pointsInheritColor", "pointColor", "linkColor", "opacity", "blending" ]; /** * @param {Object} data - Object with nodes and links properties based on https://observablehq.com/@d3/force-directed-graph-component * @param {Function} callback * @description Set the data to an instance of force directed graph. Because of the potential large amount of data this function runs on a request animation frame and returns a promise (or a passed callback) to give indication when the graph is ready to be rendered. * @returns {Promise} */ set(data, callback) { const scope = this; let { gpgpu, registry, renderer, uniforms } = this.userData; this.ready = false; this.userData.data = data; registry.clear(); if (gpgpu) { for (let i = 0; i < gpgpu.variables.length; i++) { const variable = gpgpu.variables[i]; for (let j = 0; j < variable.renderTargets.length; j++) { const renderTarget = variable.renderTargets[j]; renderTarget.dispose(); } variable.initialValueTexture.dispose(); variable.material.dispose(); } } for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; this.remove(child); if ("dispose" in child) { child.dispose(); } } const size2 = getPotSize(Math.max(data.nodes.length, data.links.length)); uniforms.size.value = size2; gpgpu = new GPUComputationRenderer(size2, size2, renderer); const textures = { positions: gpgpu.createTexture(), velocities: gpgpu.createTexture(), links: gpgpu.createTexture() }; const variables = { positions: gpgpu.addVariable( "texturePositions", simulation_default.positions, textures.positions ), velocities: gpgpu.addVariable( "textureVelocities", simulation_default.velocities, textures.velocities ) }; this.userData.gpgpu = gpgpu; this.userData.variables = variables; return register().then(fill).then(setup).then(generate).then(complete).catch((error) => { console.warn("Force Directed Graph:", error); }); function register() { return each(data.nodes, (node, i) => { registry.set(i, node); }); } function fill() { let k = 0; return each( textures.positions.image.data, (_, i) => { const x = Math.random() * 2 - 1; const y = Math.random() * 2 - 1; const z = Math.random() * 2 - 1; if (k < data.nodes.length) { const node = data.nodes[k]; textures.positions.image.data[i + 0] = typeof node.x !== "undefined" ? node.x : x; textures.positions.image.data[i + 1] = typeof node.y !== "undefined" ? node.y : y; textures.positions.image.data[i + 2] = typeof node.z !== "undefined" ? node.z : z; textures.positions.image.data[i + 3] = node.isStatic ? 1 : 0; } else { textures.positions.image.data[i + 0] = uniforms.frustumSize.value * 10; textures.positions.image.data[i + 1] = uniforms.frustumSize.value * 10; textures.positions.image.data[i + 2] = uniforms.frustumSize.value * 10; textures.positions.image.data[i + 3] = uniforms.frustumSize.value * 10; } let i1, i2, uvx, uvy; if (k < data.links.length) { i1 = registry.get(data.links[k].source); i2 = registry.get(data.links[k].target); data.links[k].sourceIndex = i1; data.links[k].targetIndex = i2; uvx = i1 % size2 / size2; uvy = Math.floor(i1 / size2) / size2; textures.links.image.data[i + 0] = uvx; textures.links.image.data[i + 1] = uvy; uvx = i2 % size2 / size2; uvy = Math.floor(i2 / size2) / size2; textures.links.image.data[i + 2] = uvx; textures.links.image.data[i + 3] = uvy; } k++; }, 4 ); } function setup() { return new Promise((resolve, reject) => { gpgpu.setVariableDependencies(variables.positions, [ variables.positions, variables.velocities ]); gpgpu.setVariableDependencies(variables.velocities, [ variables.velocities, variables.positions ]); variables.positions.material.uniforms.is2D = uniforms.is2D; variables.positions.material.uniforms.timeStep = uniforms.timeStep; variables.velocities.material.uniforms.alpha = uniforms.alpha; variables.velocities.material.uniforms.is2D = uniforms.is2D; variables.velocities.material.uniforms.size = uniforms.size; variables.velocities.material.uniforms.time = uniforms.time; variables.velocities.material.uniforms.nodeRadius = uniforms.nodeRadius; variables.velocities.material.uniforms.nodeAmount = { value: data.nodes.length }; variables.velocities.material.uniforms.edgeAmount = { value: data.links.length }; variables.velocities.material.uniforms.maxSpeed = uniforms.maxSpeed; variables.velocities.material.uniforms.timeStep = uniforms.timeStep; variables.velocities.material.uniforms.damping = uniforms.damping; variables.velocities.material.uniforms.repulsion = uniforms.repulsion; variables.velocities.material.uniforms.textureLinks = { value: textures.links }; variables.velocities.material.uniforms.springLength = uniforms.springLength; variables.velocities.material.uniforms.stiffness = uniforms.stiffness; variables.velocities.material.uniforms.gravity = uniforms.gravity; variables.positions.wrapS = variables.positions.wrapT = RepeatWrapping; variables.velocities.wrapS = variables.velocities.wrapT = RepeatWrapping; const error = gpgpu.init(); if (error) { reject(error); } else { resolve(); } }); } function generate() { let points2; return Points.parse(size2, data).then((geometry) => { points2 = new Points(geometry, uniforms); }).then(() => Links.parse(points2, data)).then((geometry) => { const links2 = new Links(geometry, uniforms); scope.add(points2, links2); points2.renderOrder = links2.renderOrder + 1; scope.userData.hit.inherit(points2); }); } function complete() { scope.ready = true; if (callback) { callback(); } } } /** * @param {Number} time * @description Function to update the instance meant to be run before three.js's renderer.render method. * @returns {Void} */ update(time) { if (!this.ready) { return this; } const { gpgpu, variables, uniforms } = this.userData; uniforms.alpha.value *= uniforms.decay.value; variables.velocities.material.uniforms.time.value = time / 1e3; gpgpu.compute(); const texture = this.getTexture("positions"); for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; child.material.uniforms.texturePositions.value = texture; } return this; } /** * @param {THREE.Vector2} pointer - x, y values normalized to the camera's clipspace * @param {THREE.Camera} camera - the camera to reference ray casting matrices * @description Check to see if a point in the browser's screenspace intersects with any points in the force directed graph. If none found, then null is returned. * @returns {Object|Null} */ intersect(pointer, camera) { const { hit: hit2, renderer } = this.userData; renderer.getSize(size); hit2.setSize(size.x, size.y); hit2.compute(renderer, camera); const x = hit2.ratio * size.x * clamp(pointer.x, 0, 1); const y = hit2.ratio * size.y * (1 - clamp(pointer.y, 0, 1)); renderer.readRenderTargetPixels( hit2.renderTarget, x - 0.5, y - 0.5, 1, 1, buffers.int ); const [r, g, b, a] = buffers.int; const z = 0; const w = 255; const isBlack = r === z && g === z && b === z && a === z; const isWhite = r === w && g === w && b === w && a === w; if (isBlack || isWhite) { return null; } const index = rgbToIndex({ r, g, b }) - 1; const point = this.getPositionFromIndex(index); return { point, data: this.userData.data.nodes[index] }; } getTexture(name) { const { gpgpu, variables } = this.userData; return gpgpu.getCurrentRenderTarget(variables[name]).texture; } getPositionFromIndex(i) { const { points: points2, size: size2 } = this; const { gpgpu, renderer, variables } = this.userData; if (!points2 || !renderer || !size2) { console.warn( "Force Directed Graph:", "unable to calculate position without points or renderer." ); return; } const index = i * 3; const uvs = points2.geometry.attributes.position.array; const uvx = Math.floor(uvs[index + 0] * size2); const uvy = Math.floor(uvs[index + 1] * size2); const renderTarget = gpgpu.getCurrentRenderTarget(variables.positions); renderer.readRenderTargetPixels( renderTarget, uvx, uvy, 1, 1, buffers.float ); const [x, y, z] = buffers.float; position.set(x, y, z); return position; } setPointColorById(id, css) { const index = this.getIndexById(id); this.setPointColorFromIndex(index, css); } setPointColorFromIndex(index, css) { const attribute = this.points.geometry.getAttribute("color"); const colors = attribute.array; color3.set(css); colors[3 * index + 0] = color3.r; colors[3 * index + 1] = color3.g; colors[3 * index + 2] = color3.b; attribute.needsUpdate = true; } updateLinksColors() { const { data } = this.userData; const ref = this.points.geometry.attributes.color.array; const attribute = this.links.geometry.getAttribute("color"); const colors = attribute.array; return each(data.links, (_, i) => { const l = data.links[i]; const li = i * 6; const si = 3 * l.sourceIndex; const ti = 3 * l.targetIndex; colors[li + 0] = ref[si + 0]; colors[li + 1] = ref[si + 1]; colors[li + 2] = ref[si + 2]; colors[li + 3] = ref[ti + 0]; colors[li + 4] = ref[ti + 1]; colors[li + 5] = ref[ti + 2]; }).then(() => attribute.needsUpdate = true); } getIndexById(id) { const { registry } = this.userData; return registry.get(id); } getLinksById(id) { const { data } = this.userData; const index = this.getIndexById(id); const result = []; const promise = each(data.links, (link2) => { const { sourceIndex, targetIndex } = link2; if (sourceIndex === index || targetIndex === index) { result.push(link2); } }); return promise.then(() => result); } getPointById(id) { const { data } = this.userData; const index = this.getIndexById(id); return data.nodes[index]; } dispose() { const { gpgpu } = this.userData; if (gpgpu) { for (let i = 0; i < gpgpu.variables.length; i++) { const variable = gpgpu.variables[i]; variable.material.dispose(); variable.initialValueTexture.dispose(); for (let j = 0; j < variable.renderTargets.length; j++) { const target = variable.renderTargets[j]; target.dispose(); } } } this.userData = {}; return this; } // Getters / Setters get decay() { return this.userData.uniforms.decay.value; } set decay(v) { this.userData.uniforms.decay.value = v; } get alpha() { return this.userData.uniforms.alpha.value; } set alpha(v) { this.userData.uniforms.alpha.value = v; } get is2D() { return this.userData.uniforms.is2D.value; } set is2D(v) { this.userData.uniforms.is2D.value = v; } get time() { return this.userData.uniforms.time.value; } set time(v) { this.userData.uniforms.time.value = v; } get size() { return this.userData.uniforms.size.value; } set size(v) { this.userData.uniforms.size.value = v; } get maxSpeed() { return this.userData.uniforms.maxSpeed.value; } set maxSpeed(v) { this.userData.uniforms.maxSpeed.value = v; } get timeStep() { return this.userData.uniforms.timeStep.value; } set timeStep(v) { this.userData.uniforms.timeStep.value = v; } get damping() { return this.userData.uniforms.damping.value; } set damping(v) { this.userData.uniforms.damping.value = v; } get repulsion() { return this.userData.uniforms.repulsion.value; } set repulsion(v) { this.userData.uniforms.repulsion.value = v; } get springLength() { return this.userData.uniforms.springLength.value; } set springLength(v) { this.userData.uniforms.springLength.value = v; } get stiffness() { return this.userData.uniforms.stiffness.value; } set stiffness(v) { this.userData.uniforms.stiffness.value = v; } get gravity() { return this.userData.uniforms.gravity.value; } set gravity(v) { this.userData.uniforms.gravity.value = v; } get nodeRadius() { return this.userData.uniforms.nodeRadius.value; } set nodeRadius(v) { this.userData.uniforms.nodeRadius.value = v; } get nodeScale() { return this.userData.uniforms.nodeScale.value; } set nodeScale(v) { this.userData.uniforms.nodeScale.value = v; } get sizeAttenuation() { return this.userData.uniforms.sizeAttenuation.value; } set sizeAttenuation(v) { this.userData.uniforms.sizeAttenuation.value = v; } get frustumSize() { return this.userData.uniforms.frustumSize.value; } set frustumSize(v) { this.userData.uniforms.frustumSize.value = v; } get linksInheritColor() { return this.userData.uniforms.linksInheritColor.value; } set linksInheritColor(v) { this.userData.uniforms.linksInheritColor.value = v; } get pointsInheritColor() { return this.userData.uniforms.pointsInheritColor.value; } set pointsInheritColor(v) { this.userData.uniforms.pointsInheritColor.value = v; } get pointColor() { return this.userData.uniforms.pointColor.value; } set pointColor(v) { this.userData.uniforms.pointColor.value = v; } get linksColor() { return this.linkColor; } set linksColor(v) { this.linkColor = v; } get linkColor() { return this.userData.uniforms.linkColor.value; } set linkColor(v) { this.userData.uniforms.linkColor.value = v; } get opacity() { return this.userData.uniforms.opacity.value; } set opacity(v) { this.userData.uniforms.opacity.value = v; } get blending() { return this.children[0].material.blending; } set blending(v) { for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; child.material.blending = v; } } get points() { return this.children[0]; } get links() { return this.children[1]; } get uniforms() { return this.userData.uniforms; } get nodeCount() { const { variables } = this.userData; return variables.velocities.material.uniforms.nodeAmount.value; } get edgeCount() { const { variables } = this.userData; return variables.velocities.material.uniforms.edgeAmount.value; } }; export { ForceDirectedGraph };