UNPKG

@jonobr1/force-directed-graph

Version:

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

1,709 lines (1,538 loc) 105 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.js var index_exports = {}; __export(index_exports, { ForceDirectedGraph: () => ForceDirectedGraph }); module.exports = __toCommonJS(index_exports); var import_three6 = require("three"); var import_GPUComputationRenderer = require("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 getUVFromIndex = ` vec2 getUVFromIndex( float i ) { float uvx = mod( i, size ) / size; float uvy = floor( i / size ) / size; return vec2( uvx, uvy ); } `; 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( int id1, vec2 uv2, float rangeStart, float rangeEnd ) { 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; float siInRange = step( rangeStart, siF ) * ( 1.0 - step( rangeEnd, siF ) ); float tiInRange = step( rangeStart, tiF ) * ( 1.0 - step( rangeEnd, tiF ) ); return result * siInRange * tiInRange; } `; 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; } `; var anchor = ` vec3 anchor( vec3 p1, vec3 target ) { return ( target - 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 float pinStrength; uniform float uBeginning; uniform float uEnding; uniform sampler2D textureLinks; uniform sampler2D textureLinkRanges; uniform sampler2D textureTargetPositions; ${getPosition} ${getVelocity} ${getIndex} ${getUVFromIndex} ${random} ${jiggle} ${link} ${charge} ${center} ${anchor} void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; int id1 = getIndex( uv ); vec3 p1 = getPosition( uv ); vec3 v1 = getVelocity( uv ); float rangeStart = uBeginning * nodeAmount; float rangeEnd = uEnding * nodeAmount; vec3 a = vec3( 0.0 ), b = vec3( 0.0 ), c = vec3( 0.0 ); vec4 linkRange = texture2D( textureLinkRanges, uv ); float linkStart = linkRange.x; float linkCount = linkRange.y; for ( float i = 0.0; i < edgeAmount; i += 1.0 ) { if ( i >= linkCount ) { break; } vec2 linkUV = getUVFromIndex( linkStart + i ); b += link( id1, linkUV, rangeStart, rangeEnd ); } for ( float i = 0.0; i < nodeAmount; i += 1.0 ) { vec2 uv2 = getUVFromIndex( i ); int id2 = getIndex( uv2 ); vec3 v2 = getVelocity( uv2 ); vec3 p2 = getPosition( uv2 ); float id2InRange = step( rangeStart, i ) * ( 1.0 - step( rangeEnd, i ) ); c += charge( i, id1, p1, v1, id2, p2, v2 ) * id2InRange; } float id1InRange = step( rangeStart, float( id1 ) ) * ( 1.0 - step( rangeEnd, float( id1 ) ) ); b *= id1InRange; c *= id1InRange; // 4. vec4 targetTexel = texture2D( textureTargetPositions, uv ); vec3 d = mix( center( p1 ), anchor( p1, targetTexel.xyz ), pinStrength * targetTexel.w ); vec3 acceleration = a + b + c + d * id1InRange; // 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 float pinStrength; uniform float uBeginning; uniform float uEnding; uniform sampler2D textureLinks; uniform sampler2D textureLinksLookUp; uniform sampler2D textureTargetPositions; ${getPosition} ${getVelocity} ${getIndex} ${getUVFromIndex} ${random} ${jiggle} ${link} ${charge} ${center} ${anchor} void main() { vec2 uv = gl_FragCoord.xy / resolution.xy; int id1 = getIndex( uv ); vec3 p1 = getPosition( uv ); vec3 v1 = getVelocity( uv ); float rangeStart = uBeginning * nodeAmount; float rangeEnd = uEnding * nodeAmount; 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( id1, uv2, rangeStart, rangeEnd ); } */ 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 ); float id2InRange = step( rangeStart, i ) * ( 1.0 - step( rangeEnd, i ) ); if ( i < nodeAmount) { c += charge( i, id1, p1, v1, id2, p2, v2 ) * id2InRange; } } float id1InRange = step( rangeStart, float( id1 ) ) * ( 1.0 - step( rangeEnd, float( id1 ) ) ); b *= id1InRange; c *= id1InRange; // 4. vec4 targetTexel = texture2D( textureTargetPositions, uv ); vec3 d = mix( center( p1 ), anchor( p1, targetTexel.xyz ), pinStrength * targetTexel.w ); vec3 acceleration = a + b + c + d * id1InRange; // 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 var import_three2 = require("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 float uBeginning; uniform float uEnding; uniform float uNodeAmount; uniform sampler2D texturePositions; uniform sampler2D textureTargetPositions; varying vec3 vColor; varying float vImageKey; varying float vDistance; varying float vViewZ; varying vec3 vTargetPosition; varying float vHasTarget; attribute float imageKey; attribute float pointSize; void main() { float nodeIndex = position.z - 1.0; float rangeStart = uBeginning * uNodeAmount; float rangeEnd = uEnding * uNodeAmount; float inRange = step( rangeStart, nodeIndex ) * ( 1.0 - step( rangeEnd, nodeIndex ) ); vec4 texel = texture2D( texturePositions, position.xy ); vec3 vPosition = texel.xyz; vPosition.z *= 1.0 - is2D; vec4 targetTexel = texture2D( textureTargetPositions, position.xy ); vTargetPosition = targetTexel.xyz; vHasTarget = targetTexel.w; vec4 mvPosition = modelViewMatrix * vec4( vPosition, 1.0 ); gl_PointSize = nodeRadius * pointSize * nodeScale; gl_PointSize *= mix( 1.0, frustumSize / - mvPosition.z, sizeAttenuation ); gl_PointSize *= inRange; vDistance = 1.0 / - mvPosition.z; vViewZ = 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; varying float vViewZ; void main() { // Calculate distance from center for circular shape and depth vec2 cxy = 2.0 * gl_PointCoord - 1.0; float r = length(cxy); // Antialiased circle using fwidth for automatic edge smoothing float delta = fwidth(r); float t = 1.0 - smoothstep(1.0 - delta, 1.0, r); // Calculate custom depth to fix z-fighting with transparent points // For fragments inside the circle, offset depth proportionally #if defined(GL_EXT_frag_depth) if (r <= 1.0) { // Keep the center of the node slightly closer so coincident links // do not leak through overlapping nodes. float depthOffset = (1.0 - r) * 0.0001; gl_FragDepthEXT = gl_FragCoord.z - depthOffset; } else { gl_FragDepthEXT = gl_FragCoord.z; } #elif __VERSION__ >= 300 if (r <= 1.0) { // Keep the center of the node slightly closer so coincident links // do not leak through overlapping nodes. float depthOffset = (1.0 - r) * 0.0001; gl_FragDepth = gl_FragCoord.z - depthOffset; } else { gl_FragDepth = gl_FragCoord.z; } #endif // Calculate texture atlas coordinates for image sprites float col = mod( vImageKey, imageDimensions ); float row = floor( vImageKey / imageDimensions ); vec2 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 var import_three = require("three"); var anchor2; var TextureAtlas = class _TextureAtlas extends import_three.Texture { map = []; dimensions = 1; isTextureAtlas = true; constructor() { if (!anchor2) { anchor2 = document.createElement("a"); } super(document.createElement("canvas")); this.flipY = false; } static Resolution = 1024; static getAbsoluteURL(path) { anchor2.href = path; return anchor2.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 import_three2.Color(); var Points = class extends import_three2.Points { constructor({ atlas, geometry }, uniforms) { const material = new import_three2.ShaderMaterial({ uniforms: { ...import_three2.UniformsLib["fog"], ...{ is2D: uniforms.is2D, sizeAttenuation: uniforms.sizeAttenuation, frustumSize: uniforms.frustumSize, nodeRadius: uniforms.nodeRadius, nodeScale: uniforms.nodeScale, imageDimensions: { value: atlas.dimensions }, texturePositions: { value: null }, textureTargetPositions: { value: null }, textureAtlas: { value: atlas }, size: uniforms.size, opacity: uniforms.opacity, uColor: uniforms.pointColor, inheritColors: uniforms.pointsInheritColor, uBeginning: uniforms.uBeginning, uEnding: uniforms.uEnding, uNodeAmount: uniforms.uNodeAmount } }, 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 vertices2 = []; const colors = []; const imageKeys = []; const sizes = []; 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; vertices2.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); } sizes.push(node.size != null ? node.size : 1); }).then(() => { const geometry = new import_three2.BufferGeometry(); geometry.setAttribute( "position", new import_three2.Float32BufferAttribute(vertices2, 3) ); geometry.setAttribute( "color", new import_three2.Float32BufferAttribute(colors, 3) ); geometry.setAttribute( "imageKey", new import_three2.Float32BufferAttribute(imageKeys, 1) ); geometry.setAttribute( "pointSize", new import_three2.Float32BufferAttribute(sizes, 1) ); return { atlas, geometry }; }); } }; // src/links.js var import_three3 = require("three"); // src/shaders/links.js var links = { vertexShader: ` #include <fog_pars_vertex> uniform float frustumSize; uniform float is2D; uniform float linewidth; uniform float pixelRatio; uniform float sizeAttenuation; uniform float uBeginning; uniform float uEnding; uniform float uNodeAmount; uniform vec2 resolution; uniform sampler2D texturePositions; attribute vec3 source; attribute vec3 target; attribute vec3 sourceColor; attribute vec3 targetColor; varying vec2 vSource; varying vec2 vTarget; varying vec3 vSourceColor; varying vec3 vTargetColor; varying float vHalfWidth; varying float inRange; void main() { float sourceIndex = source.z - 1.0; float targetIndex = target.z - 1.0; float rangeStart = uBeginning * uNodeAmount; float rangeEnd = uEnding * uNodeAmount; float sourceInRange = step( rangeStart, sourceIndex ) * ( 1.0 - step( rangeEnd, sourceIndex ) ); float targetInRange = step( rangeStart, targetIndex ) * ( 1.0 - step( rangeEnd, targetIndex ) ); vec3 sourcePosition = texture2D( texturePositions, source.xy ).xyz; vec3 targetPosition = texture2D( texturePositions, target.xy ).xyz; sourcePosition.z *= 1.0 - is2D; targetPosition.z *= 1.0 - is2D; vec4 sourceModelView = modelViewMatrix * vec4( sourcePosition, 1.0 ); vec4 targetModelView = modelViewMatrix * vec4( targetPosition, 1.0 ); vec4 sourceClip = projectionMatrix * sourceModelView; vec4 targetClip = projectionMatrix * targetModelView; vec2 safeResolution = max( resolution, vec2( 1.0 ) ); vec2 sourceNdc = sourceClip.xy / sourceClip.w; vec2 targetNdc = targetClip.xy / targetClip.w; vec2 sourceScreen = ( sourceNdc * 0.5 + 0.5 ) * safeResolution; vec2 targetScreen = ( targetNdc * 0.5 + 0.5 ) * safeResolution; vec2 delta = targetScreen - sourceScreen; float segmentLength = length( delta ); vec2 tangent = segmentLength > 0.0 ? delta / segmentLength : vec2( 1.0, 0.0 ); vec2 normal = vec2( - tangent.y, tangent.x ); float centerViewZ = 0.5 * ( sourceModelView.z + targetModelView.z ); float widthScale = mix( 1.0, frustumSize / max( -centerViewZ, 0.0001 ), sizeAttenuation ); float halfWidth = max( 0.5 * linewidth * pixelRatio * widthScale, 0.5 ); float expansion = halfWidth + 1.0; float edgeT = position.x * 0.5 + 0.5; vec2 base = mix( sourceScreen, targetScreen, edgeT ); vec2 screen = base + tangent * position.x * expansion + normal * position.y * expansion; vec2 ndc = ( screen / safeResolution ) * 2.0 - 1.0; float clipW = mix( sourceClip.w, targetClip.w, edgeT ); float clipZ = mix( sourceClip.z, targetClip.z, edgeT ); gl_Position = vec4( ndc * clipW, clipZ, clipW ); vec4 mvPosition = mix( sourceModelView, targetModelView, edgeT ); vSource = sourceScreen; vTarget = targetScreen; vSourceColor = sourceColor; vTargetColor = targetColor; vHalfWidth = halfWidth; inRange = sourceInRange * targetInRange; #include <fog_vertex> } `, fragmentShader: ` #include <fog_pars_fragment> uniform float inheritColors; uniform float linecap; uniform vec3 uColor; uniform float opacity; varying vec2 vSource; varying vec2 vTarget; varying vec3 vSourceColor; varying vec3 vTargetColor; varying float vHalfWidth; varying float inRange; float getSegmentT( vec2 point, vec2 start, vec2 end ) { vec2 segment = end - start; float lengthSquared = dot( segment, segment ); if ( lengthSquared <= 0.0 ) { return 0.0; } return clamp( dot( point - start, segment ) / lengthSquared, 0.0, 1.0 ); } float getCapsuleDistance( vec2 point, vec2 start, vec2 end, float radius ) { float t = getSegmentT( point, start, end ); vec2 closest = mix( start, end, t ); return length( point - closest ) - radius; } float getRectDistance( vec2 point, vec2 start, vec2 end, vec2 extent ) { vec2 segment = end - start; float segmentLength = length( segment ); if ( segmentLength <= 0.0 ) { return length( point - start ) - extent.y; } vec2 tangent = segment / segmentLength; vec2 normal = vec2( - tangent.y, tangent.x ); vec2 local = vec2( dot( point - start, tangent ) - 0.5 * segmentLength, dot( point - start, normal ) ); vec2 delta = abs( local ) - extent; return length( max( delta, 0.0 ) ) + min( max( delta.x, delta.y ), 0.0 ); } float getLinkDistance( vec2 point, vec2 start, vec2 end, float radius ) { vec2 segment = end - start; float segmentLength = length( segment ); if ( segmentLength <= 0.0 ) { return length( point - start ) - radius; } if ( linecap < 0.5 ) { return getCapsuleDistance( point, start, end, radius ); } if ( linecap < 1.5 ) { return getRectDistance( point, start, end, vec2( 0.5 * segmentLength, radius ) ); } return getRectDistance( point, start, end, vec2( 0.5 * segmentLength + radius, radius ) ); } void main() { if ( inRange < 0.5 ) { discard; } float segmentT = getSegmentT( gl_FragCoord.xy, vSource, vTarget ); float distanceToCapsule = getLinkDistance( gl_FragCoord.xy, vSource, vTarget, vHalfWidth ); float alpha = 1.0 - smoothstep( 0.0, 1.0, distanceToCapsule ); if ( alpha <= 0.0 ) { discard; } vec3 gradient = mix( vSourceColor, vTargetColor, segmentT ); gl_FragColor = vec4( mix( vec3( 1.0 ), gradient, inheritColors ) * uColor, opacity * alpha ); #include <fog_fragment> } ` }; var links_default = links; // src/links.js var vertices = new Float32Array([-1, -1, 0, 1, -1, 0, -1, 1, 0, 1, 1, 0]); var indices = [0, 1, 2, 2, 1, 3]; var Links = class extends import_three3.Mesh { constructor(geometry, uniforms) { const material = new import_three3.ShaderMaterial({ uniforms: { ...import_three3.UniformsLib["fog"], ...{ frustumSize: uniforms.frustumSize, is2D: uniforms.is2D, inheritColors: uniforms.linksInheritColor, linecap: uniforms.linecap, linewidth: uniforms.linewidth, opacity: uniforms.opacity, pixelRatio: uniforms.pixelRatio, resolution: uniforms.resolution, sizeAttenuation: uniforms.sizeAttenuation, texturePositions: { value: null }, uColor: uniforms.linkColor, uBeginning: uniforms.uBeginning, uEnding: uniforms.uEnding, uNodeAmount: uniforms.uNodeAmount } }, vertexShader: links_default.vertexShader, fragmentShader: links_default.fragmentShader, transparent: true, fog: true, side: import_three3.DoubleSide }); super(geometry, material); this.frustumCulled = false; } static parse(points2, data) { const geometry = new import_three3.InstancedBufferGeometry(); const sources = []; const targets = []; const sourceColors = []; const targetColors = []; const v = points2.geometry.attributes.position.array; const c = points2.geometry.attributes.color.array; geometry.setAttribute("position", new import_three3.BufferAttribute(vertices, 3)); geometry.setIndex(indices); return each(data.links, (_, i) => { const link2 = data.links[i]; const sourceIndex = 3 * link2.sourceIndex; const targetIndex = 3 * link2.targetIndex; sources.push(v[sourceIndex + 0], v[sourceIndex + 1], v[sourceIndex + 2]); targets.push(v[targetIndex + 0], v[targetIndex + 1], v[targetIndex + 2]); sourceColors.push( c[sourceIndex + 0], c[sourceIndex + 1], c[sourceIndex + 2] ); targetColors.push( c[targetIndex + 0], c[targetIndex + 1], c[targetIndex + 2] ); }).then(() => { geometry.setAttribute( "source", new import_three3.InstancedBufferAttribute(new Float32Array(sources), 3) ); geometry.setAttribute( "target", new import_three3.InstancedBufferAttribute(new Float32Array(targets), 3) ); geometry.setAttribute( "sourceColor", new import_three3.InstancedBufferAttribute(new Float32Array(sourceColors), 3) ); geometry.setAttribute( "targetColor", new import_three3.InstancedBufferAttribute(new Float32Array(targetColors), 3) ); geometry.instanceCount = data.links.length; return geometry; }); } }; // src/labels.js var import_three4 = require("three"); // src/shaders/labels.js var labels = { vertexShader: ` #include <fog_pars_vertex> uniform sampler2D texturePositions; uniform float frustumSize; uniform float is2D; uniform float sizeAttenuation; uniform vec2 resolution; uniform float uBeginning; uniform float uEnding; uniform float uNodeAmount; uniform float obscurity; uniform float nodeRadius; uniform float nodeScale; uniform float uLabelCount; uniform float labelAlignment; uniform float labelBaseline; uniform float labelFontSize; uniform float labelNear; uniform vec2 labelOffset; attribute vec3 source; // .xy = UV into texturePositions, .z = nodeIndex + 1 attribute vec4 labelUV; // .xy = atlas UV offset, .zw = atlas UV extent attribute float aspectRatio; // label quad width / height attribute float pointSize; // per-node point size scalar attribute float selectionRank; varying vec2 vLabelUV; varying vec3 vColor; varying float vInRange; void main() { float nodeIndex = source.z - 1.0; float rangeStart = uBeginning * uNodeAmount; float rangeEnd = uEnding * uNodeAmount; float inRange = step( rangeStart, nodeIndex ) * ( 1.0 - step( rangeEnd, nodeIndex ) ); float visibleCount = floor( ( 1.0 - clamp( obscurity, 0.0, 1.0 ) ) * uLabelCount + 0.5 ); float rankVisible = step( selectionRank + 0.5, visibleCount ); inRange *= rankVisible; vec3 nodePos = texture2D( texturePositions, source.xy ).xyz; nodePos.z *= 1.0 - is2D; vec4 mvCenter = modelViewMatrix * vec4( nodePos, 1.0 ); float viewDistance = -mvCenter.z; float beyondNear = 1.0 - step( viewDistance, max( labelNear, 0.0 ) ); inRange *= beyondNear; // Billboard: extract camera right and up from the corresponding view matrix rows vec3 right = normalize( vec3( viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0] ) ); vec3 up = normalize( vec3( viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1] ) ); // Match point-sprite sizing by converting the intended screen-space // label height into world units for the active projection. float sizeScale = mix( 1.0, frustumSize / max( viewDistance, 0.001 ), sizeAttenuation ); float labelPixelH = 0.1 * nodeRadius * pointSize * nodeScale * sizeScale * max( labelFontSize, 0.001 ); float projectionScaleY = max( abs( projectionMatrix[1][1] ), 0.0001 ); float isPerspectiveCamera = step( 0.5, abs( projectionMatrix[2][3] ) ); float depthScale = mix( 1.0, viewDistance, isPerspectiveCamera ); float worldUnitsPerPixel = ( 2.0 * depthScale ) / max( projectionScaleY * max( resolution.y, 1.0 ), 0.001 ); float labelH = labelPixelH * worldUnitsPerPixel; float labelW = labelH * aspectRatio; vec2 offset = labelOffset * labelH; // Shift the label relative to the node according to baseline/alignment. vec3 worldPos = nodePos + right * ( labelW * 0.5 * labelAlignment + offset.x ) + up * ( labelH * labelBaseline + offset.y ) + right * position.x * labelW * 0.5 + up * position.y * labelH * 0.5; // Map quad UV [0,1] to the atlas region for this label vLabelUV = labelUV.xy + uv * labelUV.zw; vColor = color; vInRange = inRange; vec4 mvPosition = modelViewMatrix * vec4( worldPos, 1.0 ); gl_Position = projectionMatrix * mvPosition; #include <fog_vertex> } `, fragmentShader: ` #include <fog_pars_fragment> uniform sampler2D textureAtlas; uniform float inheritColors; uniform float opacity; uniform vec3 uColor; varying vec2 vLabelUV; varying vec3 vColor; varying float vInRange; void main() { if ( vInRange <= 0.0 ) { discard; } vec4 texel = texture2D( textureAtlas, vLabelUV ); float alpha = opacity * texel.a; if ( alpha <= 0.0 ) { discard; } gl_FragColor = vec4( texel.rgb * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha ); #include <fog_fragment> #ifdef USE_FOG if ( fogFactor > 0.5 ) { discard; } #endif } ` }; var labels_default = labels; // src/labels.js var MODEL_VIEW_MATRIX = new import_three4.Matrix4(); var CAMERA_RIGHT = new import_three4.Vector3(); var CAMERA_UP = new import_three4.Vector3(); var LOCAL_NODE = new import_three4.Vector3(); var WORLD_CENTER = new import_three4.Vector3(); var WORLD_CORNER = new import_three4.Vector3(); var PROJECTED_CORNER = new import_three4.Vector3(); var MV_CENTER = new import_three4.Vector4(); var BASE_ATLAS_FONT_SIZE = 120; var BASE_ATLAS_PADDING = 4; var ATLAS_RASTER_SCALE = 2; var DEFAULT_MAX_LABEL_CANVAS_SIZE = 4096; var DEFAULT_FONT_FAMILY = "Arial, sans-serif"; var LABEL_GRAPH_DISTANCE_HOPS = 6; var LABEL_NODE_COLOR = new import_three4.Color(); var LabelAlignmentMap = { center: 0, left: 1, right: -1 }; var LabelBaselineMap = { top: 1, middle: 0, bottom: -1 }; function getLabelAlignmentName(value) { if (value > 0.5) { return "left"; } if (value < -0.5) { return "right"; } return "center"; } function getLabelBaselineName(value) { if (value > 0.5) { return "top"; } if (value < -0.5) { return "bottom"; } return "middle"; } function sanitizeLabelFontSize(fontSize) { if (!Number.isFinite(fontSize)) { return 1; } return Math.max(0.01, fontSize); } function sanitizeLabelNearDistance(nearDistance) { if (!Number.isFinite(nearDistance)) { return 0; } return Math.max(0, nearDistance); } function sanitizePositiveInteger(value, fallback) { if (!Number.isFinite(value) || value <= 0) { return fallback; } return Math.max(1, Math.floor(value)); } function getLabelAtlasMaxTextureSize(options = {}) { const rendererMaxTextureSize = sanitizePositiveInteger( options.maxTextureSize, 16384 ); const canvasMaxTextureSize = sanitizePositiveInteger( options.maxCanvasTextureSize, DEFAULT_MAX_LABEL_CANVAS_SIZE ); return Math.min(rendererMaxTextureSize, canvasMaxTextureSize); } function getNodeColorComponents(node) { if (node?.color) { LABEL_NODE_COLOR.set(node.color); return [LABEL_NODE_COLOR.r, LABEL_NODE_COLOR.g, LABEL_NODE_COLOR.b]; } return [1, 1, 1]; } function compareSelectionCandidates(a, b) { if (b.hasManualPriority !== a.hasManualPriority) { return Number(b.hasManualPriority) - Number(a.hasManualPriority); } if (b.manualPriority !== a.manualPriority) { return b.manualPriority - a.manualPriority; } if (b.degree !== a.degree) { return b.degree - a.degree; } return a.entry.stableId - b.entry.stableId; } function relaxGraphDistances(sourceIndex, adjacency, distances, maxHops) { if (!Number.isInteger(sourceIndex) || sourceIndex < 0) { return; } const visited = new Int16Array(adjacency.length || distances.length); visited.fill(-1); const queue = [sourceIndex]; const depths = [0]; visited[sourceIndex] = 0; distances[sourceIndex] = 0; for (let i = 0; i < queue.length; i++) { const nodeIndex = queue[i]; const depth = depths[i]; if (depth >= maxHops) { continue; } const neighbors = adjacency[nodeIndex] || []; for (let j = 0; j < neighbors.length; j++) { const neighbor = neighbors[j]; if (!Number.isInteger(neighbor) || neighbor < 0 || neighbor >= visited.length) { continue; } if (visited[neighbor] >= 0) { continue; } const nextDepth = depth + 1; visited[neighbor] = nextDepth; if (nextDepth < distances[neighbor]) { distances[neighbor] = nextDepth; } queue.push(neighbor); depths.push(nextDepth); } } } function buildLabelSelectionOrder(entries, adjacency = [], nodes = [], degrees = [], maxHops = LABEL_GRAPH_DISTANCE_HOPS) { if (!entries || entries.length === 0) { return []; } const candidates = entries.map((entry) => { const node = nodes[entry.nodeIndex] || {}; const manualPriority = typeof node.labelPriority === "number" && Number.isFinite(node.labelPriority) ? node.labelPriority : -Infinity; return { entry, degree: typeof degrees[entry.nodeIndex] === "number" && Number.isFinite(degrees[entry.nodeIndex]) ? degrees[entry.nodeIndex] : 0, hasManualPriority: Number.isFinite(manualPriority), manualPriority, selected: false }; }).sort(compareSelectionCandidates); const distances = new Int16Array( Math.max( 1, nodes.length, adjacency.length, ...entries.map((entry) => entry.nodeIndex + 1) ) ); distances.fill(maxHops + 1); const order = []; let hasSelection = false; for (let threshold = maxHops + 1; threshold >= 0; threshold--) { for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i]; if (candidate.selected) { continue; } const distance = distances[candidate.entry.nodeIndex]; if (hasSelection && distance < threshold) { continue; } candidate.selected = true; order.push(candidate.entry); relaxGraphDistances( candidate.entry.nodeIndex, adjacency, distances, maxHops ); hasSelection = true; } } return order; } function layoutAtlasRows(items, maxTextureSize) { if (!Number.isFinite(maxTextureSize) || maxTextureSize <= 0) { return { fits: false, width: 0, height: 0, placements: [] }; } let x = 0; let y = 0; let rowHeight = 0; let maxWidth = 0; const placements = []; for (let i = 0; i < items.length; i++) { const item = items[i]; const width = Math.max(1, Math.ceil(item.labelWidth)); const height = Math.max(1, Math.ceil(item.labelHeight)); if (width > maxTextureSize || height > maxTextureSize) { return { fits: false, width: 0, height: 0, placements: [] }; } if (x > 0 && x + width > maxTextureSize) { x = 0; y += rowHeight; rowHeight = 0; } placements.push({ x, y, width, height }); x += width; rowHeight = Math.max(rowHeight, height); maxWidth = Math.max(maxWidth, x); if (y + rowHeight > maxTextureSize) { return { fits: false, width: 0, height: 0, placements: [] }; } } return { fits: true, width: Math.max(1, maxWidth), height: Math.max(1, y + rowHeight), placements }; } function measureAtlasCandidate(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, scale, maxTextureSize }) { const padding = Math.max(1, Math.round(requestedPadding * scale)); const fontSize = Math.max(1, Math.round(requestedFontSize * scale)); const tileH = fontSize + padding * 2; if (tileH > maxTextureSize) { return { fits: false }; } tempCtx.font = `${fontSize}px ${fontFamily}`; const items = rawItems.map((item) => { const labelWidth = Math.ceil(tempCtx.measureText(item.text).width) + padding * 2; return { ...item, labelWidth, labelHeight: tileH, aspectRatio: labelWidth / tileH }; }); const layout = layoutAtlasRows(items, maxTextureSize); return { fits: layout.fits, padding, fontSize, tileH, items, layout }; } function fitAtlasLayout(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, maxTextureSize }) { let lo = 0; let hi = 1; let best = null; for (let i = 0; i < 12; i++) { const scale = (lo + hi) * 0.5; const candidate = measureAtlasCandidate(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, scale, maxTextureSize }); if (candidate.fits) { best = { ...candidate, scale }; lo = scale; } else { hi = scale; } } if (best) { return best; } return measureAtlasCandidate(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, scale: 0.01, maxTextureSize }); } function buildTextAtlas(nodes, degrees = [], options = {}) { const fontScale = sanitizeLabelFontSize(options.fontSize); const atlasScale = ATLAS_RASTER_SCALE; const requestedPadding = Math.max( 1, Math.round(BASE_ATLAS_PADDING * fontScale * atlasScale) ); const requestedFontSize = Math.max( 1, Math.round(BASE_ATLAS_FONT_SIZE * fontScale * atlasScale) ); const fontFamily = options.fontFamily || DEFAULT_FONT_FAMILY; const maxTextureSize = getLabelAtlasMaxTextureSize(options); const textColor = "#fff"; const temp = document.createElement("canvas"); const tempCtx = temp.getContext("2d"); tempCtx.font = `${requestedFontSize}px ${fontFamily}`; const rawItems = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.label === null || node.label === void 0) { continue; } const text = String(node.label); rawItems.push({ text, nodeIndex: i, pointSize: typeof node.size === "number" && Number.isFinite(node.size) ? node.size : 1, basePriority: getLabelBasePriority(node, degrees[i] || 0) }); } if (rawItems.length === 0) { return null; } const fittedAtlas = fitAtlasLayout(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, maxTextureSize }); if (!fittedAtlas.fits) { return null; } const canvas = document.createElement("canvas"); canvas.width = fittedAtlas.layout.width; canvas.height = fittedAtlas.layout.height; const ctx = canvas.getContext("2d"); ctx.imageSmoothingEnabled = true; ctx.font = `${fittedAtlas.fontSize}px ${fontFamily}`; ctx.fillStyle = textColor; ctx.textBaseline = "middle"; ctx.textAlign = "center"; const entries = []; for (let i = 0; i < fittedAtlas.items.length; i++) { const item = fittedAtlas.items[i]; const placement = fittedAtlas.layout.placements[i]; const px = placement.x; const py = placement.y; ctx.fillText( item.text, px + placement.width / 2, py + placement.height / 2 ); entries.push({ ...item, labelId: i, stableId: item.nodeIndex, persistence: 0, atlasUV: { u: px / canvas.width, v: 1 - (py + placement.height) / canvas.height, uw: placement.width / canvas.width, uh: placement.height / canvas.height } }); } return { canvas, entries }; } function getLabelBasePriority(node, degree = 0) { if (typeof node.labelPriority === "number" && Number.isFinite(node.labelPriority)) { return node.labelPriority; } if (typeof node.size === "number" && Number.isFinite(node.size)) { return node.size; } if (Number.isFinite(degree)) { return degree; } return 0; } function compareLabelEntries(a, b) { if (b.basePriority !== a.basePriority) { return b.basePriority - a.basePriority; } return a.stableId - b.stableId; } function buildSelectionRanks(entries, selectionOrder) { const ranksByLabelId = new Float32Array(entries.length); const orderedEntries = selectionOrder && selectionOrder.length > 0 ? selectionOrder : entries.slice().sort(compareLabelEntries); for (let i = 0; i < orderedEntries.length; i++) { ranksByLabelId[orderedEntries[i].labelId] = i; } return ranksByLabelId; } function configureAtlasTexture(texture, options = {}) { const useMipmaps = Boolean(options.useMipmaps); texture.minFilter = useMipmaps ? import_three4.LinearMipmapLinearFilter : import_three4.LinearFilter; texture.magFilter = import_three4.LinearFilter; texture.wrapS = import_three4.ClampToEdgeWrapping; texture.wrapT = import_three4.ClampToEdgeWrapping; texture.generateMipmaps = useMipmaps; texture.needsUpdate = true; return texture; } var Labels = class extends import_three4.Mesh { constructor({ geometry, texture, entries, fontFamily }, uniforms) { const material = new import_three4.ShaderMaterial({ uniforms: { ...import_three4.UniformsLib.fog, ...{ texturePositions: { value: null }, textureAtlas: { value: texture }, opacity: uniforms.opacity, obscurity: uniforms.obscurity, frustumSize: uniforms.frustumSize, inheritColors: uniforms.labelsInheritColor, is2D: uniforms.is2D, sizeAttenuation: uniforms.sizeAttenuation, resolution: uniforms.resolution, nodeRadius: uniforms.nodeRadius, nodeScale: uniforms.nodeScale, uLabelCount: { value: entries.length }, uColor: uniforms.labelColor, labelAlignment: uniforms.labelAlignment, labelBaseline: uniforms.labelBaseline, labelFontSize: uniforms.labelFontSize, labelNear: uniforms.labelNear, labelOffset: uniforms.labelOffset, uBeginning: uniforms.uBeginning, uEnding: uniforms.uEnding, uNodeAmount: uniforms.uNodeAmount } }, vertexShader: labels_default.vertexShader, fragmentShader: labels_default.fragmentShader, transparent: true, vertexColors: true, depthWrite: false, depthTest: false, fog: true }); super(geometry, material); this.frustumCulled = false; this.entries = entries; this.userData.fontFamily = fontFamily || DEFAULT_FONT_FAMILY; this.userData.fontSize = uniforms.labelFontSize.value; this.userData.near = sanitizeLabelNearDistance(uniforms.labelNear.value); } dispose() { this.material.uniforms.textureAtlas.value?.dispose?.(); this.material.dispose(); this.geometry.dispose(); } replaceData({ geometry, texture, entries, fontFamily, fontSize }) { this.geometry.dispose(); this.material.uniforms.textureAtlas.value?.dispose?.(); this.geometry = geometry; this.entries = entries; this.material.uniforms.textureAtlas.value = texture; this.material.uniforms.uLabelCount.value = entries.length; this.userData.fontFamily = fontFamily || DEFAULT_FONT_FAMILY; this.userData.fontSize = sanitizeLabelFontSize(fontSize); this.userData.near = sanitizeLabelNearDistance( this.material.uniforms.labelNear.value ); } get fontSize() { if (this.parent?.userData?.uniforms?.labelFontSize) { return this.parent.userData.uniforms.labelFontSize.value; } return this.userData.fontSize; } set fontSize(v) { const nextValue = sanitizeLabelFontSize(v); this.userData.fontSize = nextValue; if (!this.material?.uniforms?.labelFontSize) { return; } if (this.material.uniforms.labelFontSize.value === nextValue) { return; } this.material.uniforms.labelFontSize.value = nextValue; } get fontFamily() { if (this.parent?.userData?.labelFontFamily) { return this.parent.userData.labelFontFamily; } return this.userData.fontFamily; } set fontFamily(v) { const nextValue = typeof v === "string" && v.trim().length > 0 ? v.trim() : DEFAULT_FONT_FAMILY; this.userData.fontFamily = nextValue; if (!this.parent?.userData) { return; } if (this.parent.userData.labelFontFamily === nextValue) { return; } this.parent.userData.labelFontFamily = nextValue; this.parent.refreshLabels(); } get alignment() { return getLabelAlignmentName(this.material.uniforms.labelAlignment.value); } set alignment(v) { this.material.uniforms.labelAlignment.value = LabelAlignmentMap[v] ?? LabelAlignmentMap.center; } get baseline() { return getLabelBaselineName(this.material.uniforms.labelBaseline.value); } set baseline(v) { this.material.uniforms.labelBaseline.value = LabelBaselineMap[v] ?? LabelBaselineMap.top; } get offset() { return this.material.uniforms.labelOffset.value; } set offset(v) { if (!v || !Number.isFinite(v.x) || !Number.isFinite(v.y)) { return; } this.material.uniforms.labelOffset.value.set(v.x, v.y); } get near() { if (this.parent?.userData?.uniforms?.labelNear) { return this.parent.userData.uniforms.labelNear.value; } return this.userData.near; } set near(v) { const nextValue = sanitizeLabelNearDistance(v); this.userData.near = nextValue; if (!this.material?.uniforms?.labelNear) { return; } if (this.material.uniforms.labelNear.value === nextValue) { return; } this.material.uniforms.labelNear.value = nextValue; } static parse(size2, data, options = {}) { const atlas = buildTextAtlas(data.nodes, options.degrees || [], options); if (!atlas) { return Promise.resolve(null); } const { canvas, entries } = atlas; const quadVerts = new Float32Array([ -1, -1, 0, 1, -1, 0, -1, 1, 0, 1, 1, 0 ]); const quadUVs = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); const quadIdx = [0, 1, 2, 2, 1, 3]; const geometry = new import_three4.InstancedBufferGeometry(); geometry.setAttribute("position", new import_three4.BufferAttribute(quadVerts, 3)); geometry.setAttribute("uv", new import_three4.Bu