Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/

# IDEs and editors
.idea/
.vscode/

# Optional npm cache directory
.npm
Expand Down
11 changes: 10 additions & 1 deletion docs/view-default.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ interface IOrbViewSettings {
radius: number;
}
};
// For canvas rendering and events
// For rendering and events
render: {
type: 'canvas' | 'webgl';
devicePixelRatio: number | null;
fps: number;
minZoom: number;
Expand Down Expand Up @@ -319,6 +320,7 @@ Each layout type has its own option list you can tweak in order to change the la
- `isSimulatingOnDataUpdate` - Whether to run simulation when graph data is updated. Enabled by default (`true`).
- `isSimulatingOnSettingsUpdate` - Whether to re-run simulation when settings are updated. Enabled by default (`true`).
- `isSimulatingOnUnstick` - Whether to re-run simulation when a node is unstuck. Enabled by default (`true`).
- `useGPU` - Runs the force layout on the GPU (WebGL2) using a Barnes-Hut quadtree. Falls back to the CPU engine automatically if WebGL2 is unavailable. Disabled by default (`false`).
- `alpha` - Fine-grained control over the simulation's annealing process.
- `alpha` - Current alpha value. Default `1`.
- `alphaMin` - Minimum alpha threshold below which simulation stops. Default `0.001`.
Expand Down Expand Up @@ -352,6 +354,13 @@ Each layout type has its own option list you can tweak in order to change the la
Optional property `render` has several rendering options that you can tweak. Read more about them
on [Styling guide](./styles.md).

#### Property `render.type`

Selects the rendering backend. `canvas` (default) draws with the HTML5 Canvas API, while `webgl`
uses a WebGL2 renderer that offloads drawing to the GPU for better performance on large graphs.
The backend is chosen when the view is created and cannot be changed afterwards — to switch, create
a new view. See the `example-webgl-renderer.html` example for a live comparison.

#### Property `render.devicePixelRatio`

`devicePixelRatio` is useful when dealing with the difference between rendering on a standard
Expand Down
148 changes: 148 additions & 0 deletions examples/example-webgl-renderer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang='en' type="module">
<head>
<base href=".">
<meta charset='UTF-8'>
<title>Orb | WebGL renderer & GPU layout</title>
<script type="text/javascript" src="./orb.js"></script>
</head>
<style>
html, body {
height: 100%;
margin: 0;
}
.controls {
width: 70%;
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
margin-bottom: 0.5rem;
}
.controls label {
display: flex;
gap: 0.35rem;
align-items: center;
}
</style>
<body>
<div style="display: flex; flex-direction: column; align-items: center; height: 100%;">
<h1>Example 9 - WebGL renderer & GPU layout</h1>
<p style="width: 70%">
Renders a generated graph and lets you switch between the Canvas and WebGL
renderers and the CPU and GPU force-layout engines across different graph
sizes. The GPU layout falls back to the CPU automatically if WebGL2 is
unavailable.
</p>

<div class="controls">
<label>
Renderer
<select id="renderer">
<option value="canvas">Canvas</option>
<option value="webgl" selected>WebGL</option>
</select>
</label>
<label>
Layout engine
<select id="engine">
<option value="cpu">CPU</option>
<option value="gpu" selected>GPU</option>
</select>
</label>
<label>
Nodes
<select id="size">
<option>100</option>
<option>500</option>
<option selected>2000</option>
<option>5000</option>
<option>10000</option>
<option>20000</option>
</select>
</label>
<button id="regenerate">Regenerate</button>
</div>

<!--
Make sure that your graph container has a defined width and height.
Orb will expand to any available space, but won't be visible if it's parent container is collapsed.
-->
<div id='graph' style="flex: 1; width: 100%;"></div>
</div>
<script type="text/javascript">
const container = document.getElementById('graph');
const rendererSelect = document.getElementById('renderer');
const engineSelect = document.getElementById('engine');
const sizeSelect = document.getElementById('size');
const regenerateButton = document.getElementById('regenerate');

// Generates a connected random graph: a spanning tree plus some extra edges.
const generateGraph = (nodeCount) => {
const nodes = [];
const edges = [];
for (let i = 0; i < nodeCount; i++) {
nodes.push({ id: i });
}
let edgeId = 0;
for (let i = 1; i < nodeCount; i++) {
const target = Math.floor(Math.random() * i);
edges.push({ id: edgeId++, start: i, end: target });
}
const extraEdges = Math.floor(nodeCount * 0.5);
for (let i = 0; i < extraEdges; i++) {
const start = Math.floor(Math.random() * nodeCount);
const end = Math.floor(Math.random() * nodeCount);
if (start !== end) {
edges.push({ id: edgeId++, start, end });
}
}
return { nodes, edges };
};

let orb;

const build = () => {
const type = rendererSelect.value;
const useGPU = engineSelect.value === 'gpu';
const nodeCount = Number(sizeSelect.value);

// The renderer type is fixed at construction time, so a switch rebuilds the view.
if (orb) {
orb.destroy();
}
orb = new Orb.OrbView(container, {
render: { type },
layout: { type: 'force', options: { useGPU } },
});

orb.data.setDefaultStyle({
getNodeStyle() {
return {
size: 6,
color: '#1d5fa5',
borderColor: '#0c447c',
borderWidth: 0.5,
};
},
getEdgeStyle() {
return {
width: 0.3,
color: '#cccccc',
};
},
});

orb.data.setup(generateGraph(nodeCount));
orb.render(() => orb.recenter());
};

rendererSelect.addEventListener('change', build);
engineSelect.addEventListener('change', build);
sizeSelect.addEventListener('change', build);
regenerateButton.addEventListener('click', build);

build();
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,13 @@ <h1>Orb Examples</h1>
Renders a simple graph with hierarchical layout (tree-like).
</p>
</div>
<div>
<a href="./example-webgl-renderer.html"><h3><li>Example 9 - WebGL renderer & GPU layout</li></h3></a>
<p>
Renders a generated graph and lets you switch between the Canvas and WebGL
renderers and the CPU and GPU force-layout engines across different graph
sizes.
</p>
</div>
</body>
</html>
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
{
"name": "Toni Lastre",
"url": "https://github.com/tonilastre"
},
{
"name": "Oleksandr Ichenskyi",
"url": "https://github.com/AlexIchenskiy"
}
],
"author": "Memgraph Ltd. <contact@memgraph.com>",
Expand Down Expand Up @@ -109,4 +113,4 @@
"main"
]
}
}
}
14 changes: 14 additions & 0 deletions src/glsl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
declare module '*.glsl' {
const value: string;
export default value;
}

declare module '*.vert' {
const value: string;
export default value;
}

declare module '*.frag' {
const value: string;
export default value;
}
150 changes: 150 additions & 0 deletions src/renderer/webgl/shaders/edge/edge.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#version 300 es

precision highp float;

in vec2 vWorldPos;
in vec2 vStart;
in vec2 vEnd;
in vec2 vControl;
in float vHalfWidth;
in float vLoopbackRadius;
in float vArrowSize;
in vec2 vArrowTip;
in vec2 vArrowDir;
in vec4 vColor;
in vec4 vShadowColor;
in float vShadowSize;
in vec2 vShadowOffset;
flat in int vEdgeType;

uniform bool uSimpleMode;

out vec4 fragColor;

float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}

float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C) {
vec2 a = B - A;
vec2 b = A - 2.0 * B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;

float kk = 1.0 / max(dot(b, b), 0.0001);
float kx = kk * dot(a, b);
float ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
float kz = kk * dot(d, a);

float p = ky - kx * kx;
float q = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
float p3 = p * p * p;
float q2 = q * q;
float h = q2 + 4.0 * p3;

float res;
if (h >= 0.0) {
h = sqrt(h);
vec2 x = (vec2(h, -h) - q) / 2.0;
vec2 uv = sign(x) * pow(abs(x), vec2(1.0 / 3.0));
float t = clamp(uv.x + uv.y - kx, 0.0, 1.0);
vec2 qo = d + (c + b * t) * t;
res = dot(qo, qo);
} else {
float z = sqrt(-p);
float v = acos(q / (p * z * 2.0)) / 3.0;
float m = cos(v);
float n = sin(v) * 1.732050808;
vec3 t = clamp(vec3(m + m, -n - m, n - m) * z - kx, 0.0, 1.0);
vec2 qx = d + (c + b * t.x) * t.x;
float dx = dot(qx, qx);
vec2 qy = d + (c + b * t.y) * t.y;
float dy = dot(qy, qy);
res = min(dx, dy);
}

return sqrt(res);
}

float sdArrow(vec2 p, vec2 tip, vec2 dir, float size) {
if (size <= 0.0) return 1e6;

vec2 perp = vec2(-dir.y, dir.x);
vec2 rel = p - tip;
float along = dot(rel, -dir);
float across = dot(rel, perp);

if (along < 0.0) return length(rel);
if (along > size) {
float hw = size * 0.4;
float closest = clamp(across, -hw, hw);
vec2 pt = tip - dir * size + perp * closest;
return length(p - pt);
}

float halfW = (along / size) * size * 0.4;
float d = abs(across) - halfW;
return d;
}

void main() {
if (uSimpleMode && vEdgeType == 0) {
fragColor = vColor;
return;
}

float dist;
if (vEdgeType == 0) {
dist = sdSegment(vWorldPos, vStart, vEnd);
} else if (vEdgeType == 1) {
dist = sdBezier(vWorldPos, vStart, vControl, vEnd);
} else {
dist = abs(length(vWorldPos - vControl) - vLoopbackRadius);
}

float edgeSdf = dist - vHalfWidth;
float combinedSdf = edgeSdf;

if (vArrowSize > 0.0) {
float arrowDist = sdArrow(vWorldPos, vArrowTip, vArrowDir, vArrowSize);
combinedSdf = min(edgeSdf, arrowDist);
}

float shadowAlpha = 0.0;
if (vShadowSize > 0.0) {
vec2 shadowPos = vWorldPos - vShadowOffset;
float shadowDist;
if (vEdgeType == 0) {
shadowDist = sdSegment(shadowPos, vStart, vEnd);
} else if (vEdgeType == 1) {
shadowDist = sdBezier(shadowPos, vStart, vControl, vEnd);
} else {
shadowDist = abs(length(shadowPos - vControl) - vLoopbackRadius);
}
float shadowArrowDist = vArrowSize > 0.0
? sdArrow(shadowPos, vArrowTip, vArrowDir, vArrowSize)
: 1.0e6;
float shadowCombined = min(shadowDist - vHalfWidth, shadowArrowDist);
float t = max(shadowCombined, 0.0) / vShadowSize;
shadowAlpha = exp(-t * t * 1.5) * 0.5 * vShadowColor.a;
}

float aa = fwidth(combinedSdf);
float edgeAlpha = 1.0 - smoothstep(-aa, aa, combinedSdf);
vec4 edgeColor = vColor;
edgeColor.a *= edgeAlpha;

float finalAlpha = edgeColor.a + shadowAlpha * (1.0 - edgeColor.a);

if (finalAlpha < 0.001) discard;

if (shadowAlpha > 0.0) {
vec3 finalRGB = (edgeColor.rgb * edgeColor.a + vShadowColor.rgb * shadowAlpha * (1.0 - edgeColor.a)) / finalAlpha;
fragColor = vec4(finalRGB, finalAlpha);
} else {
fragColor = edgeColor;
}
}
Loading
Loading