Personal blog written from scratch using Node.js, Bootstrap, and MySQL. https://jrtechs.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

717 lines
22 KiB

  1. ;(function(undefined) {
  2. 'use strict';
  3. if (typeof sigma === 'undefined')
  4. throw 'sigma is not declared';
  5. // Initialize packages:
  6. sigma.utils.pkg('sigma.renderers');
  7. /**
  8. * This function is the constructor of the canvas sigma's renderer.
  9. *
  10. * @param {sigma.classes.graph} graph The graph to render.
  11. * @param {sigma.classes.camera} camera The camera.
  12. * @param {configurable} settings The sigma instance settings
  13. * function.
  14. * @param {object} object The options object.
  15. * @return {sigma.renderers.canvas} The renderer instance.
  16. */
  17. sigma.renderers.webgl = function(graph, camera, settings, options) {
  18. if (typeof options !== 'object')
  19. throw 'sigma.renderers.webgl: Wrong arguments.';
  20. if (!(options.container instanceof HTMLElement))
  21. throw 'Container not found.';
  22. var k,
  23. i,
  24. l,
  25. a,
  26. fn,
  27. _self = this;
  28. sigma.classes.dispatcher.extend(this);
  29. // Conrad related attributes:
  30. this.jobs = {};
  31. Object.defineProperty(this, 'conradId', {
  32. value: sigma.utils.id()
  33. });
  34. // Initialize main attributes:
  35. this.graph = graph;
  36. this.camera = camera;
  37. this.contexts = {};
  38. this.domElements = {};
  39. this.options = options;
  40. this.container = this.options.container;
  41. this.settings = (
  42. typeof options.settings === 'object' &&
  43. options.settings
  44. ) ?
  45. settings.embedObjects(options.settings) :
  46. settings;
  47. // Find the prefix:
  48. this.options.prefix = this.camera.readPrefix;
  49. // Initialize programs hash
  50. Object.defineProperty(this, 'nodePrograms', {
  51. value: {}
  52. });
  53. Object.defineProperty(this, 'edgePrograms', {
  54. value: {}
  55. });
  56. Object.defineProperty(this, 'nodeFloatArrays', {
  57. value: {}
  58. });
  59. Object.defineProperty(this, 'edgeFloatArrays', {
  60. value: {}
  61. });
  62. Object.defineProperty(this, 'edgeIndicesArrays', {
  63. value: {}
  64. });
  65. // Initialize the DOM elements:
  66. if (this.settings(options, 'batchEdgesDrawing')) {
  67. this.initDOM('canvas', 'edges', true);
  68. this.initDOM('canvas', 'nodes', true);
  69. } else {
  70. this.initDOM('canvas', 'scene', true);
  71. this.contexts.nodes = this.contexts.scene;
  72. this.contexts.edges = this.contexts.scene;
  73. }
  74. this.initDOM('canvas', 'labels');
  75. this.initDOM('canvas', 'mouse');
  76. this.contexts.hover = this.contexts.mouse;
  77. // Initialize captors:
  78. this.captors = [];
  79. a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
  80. for (i = 0, l = a.length; i < l; i++) {
  81. fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
  82. this.captors.push(
  83. new fn(
  84. this.domElements.mouse,
  85. this.camera,
  86. this.settings
  87. )
  88. );
  89. }
  90. // Deal with sigma events:
  91. sigma.misc.bindEvents.call(this, this.camera.prefix);
  92. sigma.misc.drawHovers.call(this, this.camera.prefix);
  93. this.resize();
  94. };
  95. /**
  96. * This method will generate the nodes and edges float arrays. This step is
  97. * separated from the "render" method, because to keep WebGL efficient, since
  98. * all the camera and middlewares are modelised as matrices and they do not
  99. * require the float arrays to be regenerated.
  100. *
  101. * Basically, when the user moves the camera or applies some specific linear
  102. * transformations, this process step will be skipped, and the "render"
  103. * method will efficiently refresh the rendering.
  104. *
  105. * And when the user modifies the graph colors or positions (applying a new
  106. * layout or filtering the colors, for instance), this "process" step will be
  107. * required to regenerate the float arrays.
  108. *
  109. * @return {sigma.renderers.webgl} Returns the instance itself.
  110. */
  111. sigma.renderers.webgl.prototype.process = function() {
  112. var a,
  113. i,
  114. l,
  115. k,
  116. type,
  117. renderer,
  118. graph = this.graph,
  119. options = sigma.utils.extend(options, this.options),
  120. defaultEdgeType = this.settings(options, 'defaultEdgeType'),
  121. defaultNodeType = this.settings(options, 'defaultNodeType');
  122. // Empty float arrays:
  123. for (k in this.nodeFloatArrays)
  124. delete this.nodeFloatArrays[k];
  125. for (k in this.edgeFloatArrays)
  126. delete this.edgeFloatArrays[k];
  127. for (k in this.edgeIndicesArrays)
  128. delete this.edgeIndicesArrays[k];
  129. // Sort edges and nodes per types:
  130. for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
  131. type = a[i].type || defaultEdgeType;
  132. k = (type && sigma.webgl.edges[type]) ? type : 'def';
  133. if (!this.edgeFloatArrays[k])
  134. this.edgeFloatArrays[k] = {
  135. edges: []
  136. };
  137. this.edgeFloatArrays[k].edges.push(a[i]);
  138. }
  139. for (a = graph.nodes(), i = 0, l = a.length; i < l; i++) {
  140. type = a[i].type || defaultNodeType;
  141. k = (type && sigma.webgl.nodes[type]) ? type : 'def';
  142. if (!this.nodeFloatArrays[k])
  143. this.nodeFloatArrays[k] = {
  144. nodes: []
  145. };
  146. this.nodeFloatArrays[k].nodes.push(a[i]);
  147. }
  148. // Push edges:
  149. for (k in this.edgeFloatArrays) {
  150. renderer = sigma.webgl.edges[k];
  151. a = this.edgeFloatArrays[k].edges;
  152. // Creating the necessary arrays
  153. this.edgeFloatArrays[k].array = new Float32Array(
  154. a.length * renderer.POINTS * renderer.ATTRIBUTES
  155. );
  156. for (i = 0, l = a.length; i < l; i++) {
  157. // Just check that the edge and both its extremities are visible:
  158. if (
  159. !a[i].hidden &&
  160. !graph.nodes(a[i].source).hidden &&
  161. !graph.nodes(a[i].target).hidden
  162. )
  163. renderer.addEdge(
  164. a[i],
  165. graph.nodes(a[i].source),
  166. graph.nodes(a[i].target),
  167. this.edgeFloatArrays[k].array,
  168. i * renderer.POINTS * renderer.ATTRIBUTES,
  169. options.prefix,
  170. this.settings
  171. );
  172. }
  173. if (typeof renderer.computeIndices === 'function')
  174. this.edgeIndicesArrays[k] = renderer.computeIndices(
  175. this.edgeFloatArrays[k].array
  176. );
  177. }
  178. // Push nodes:
  179. for (k in this.nodeFloatArrays) {
  180. renderer = sigma.webgl.nodes[k];
  181. a = this.nodeFloatArrays[k].nodes;
  182. // Creating the necessary arrays
  183. this.nodeFloatArrays[k].array = new Float32Array(
  184. a.length * renderer.POINTS * renderer.ATTRIBUTES
  185. );
  186. for (i = 0, l = a.length; i < l; i++) {
  187. if (!this.nodeFloatArrays[k].array)
  188. this.nodeFloatArrays[k].array = new Float32Array(
  189. a.length * renderer.POINTS * renderer.ATTRIBUTES
  190. );
  191. // Just check that the edge and both its extremities are visible:
  192. if (
  193. !a[i].hidden
  194. )
  195. renderer.addNode(
  196. a[i],
  197. this.nodeFloatArrays[k].array,
  198. i * renderer.POINTS * renderer.ATTRIBUTES,
  199. options.prefix,
  200. this.settings
  201. );
  202. }
  203. }
  204. return this;
  205. };
  206. /**
  207. * This method renders the graph. It basically calls each program (and
  208. * generate them if they do not exist yet) to render nodes and edges, batched
  209. * per renderer.
  210. *
  211. * As in the canvas renderer, it is possible to display edges, nodes and / or
  212. * labels in batches, to make the whole thing way more scalable.
  213. *
  214. * @param {?object} params Eventually an object of options.
  215. * @return {sigma.renderers.webgl} Returns the instance itself.
  216. */
  217. sigma.renderers.webgl.prototype.render = function(params) {
  218. var a,
  219. i,
  220. l,
  221. k,
  222. o,
  223. program,
  224. renderer,
  225. self = this,
  226. graph = this.graph,
  227. nodesGl = this.contexts.nodes,
  228. edgesGl = this.contexts.edges,
  229. matrix = this.camera.getMatrix(),
  230. options = sigma.utils.extend(params, this.options),
  231. drawLabels = this.settings(options, 'drawLabels'),
  232. drawEdges = this.settings(options, 'drawEdges'),
  233. drawNodes = this.settings(options, 'drawNodes');
  234. // Call the resize function:
  235. this.resize(false);
  236. // Check the 'hideEdgesOnMove' setting:
  237. if (this.settings(options, 'hideEdgesOnMove'))
  238. if (this.camera.isAnimated || this.camera.isMoving)
  239. drawEdges = false;
  240. // Clear canvases:
  241. this.clear();
  242. // Translate matrix to [width/2, height/2]:
  243. matrix = sigma.utils.matrices.multiply(
  244. matrix,
  245. sigma.utils.matrices.translation(this.width / 2, this.height / 2)
  246. );
  247. // Kill running jobs:
  248. for (k in this.jobs)
  249. if (conrad.hasJob(k))
  250. conrad.killJob(k);
  251. if (drawEdges) {
  252. if (this.settings(options, 'batchEdgesDrawing'))
  253. (function() {
  254. var a,
  255. k,
  256. i,
  257. id,
  258. job,
  259. arr,
  260. end,
  261. start,
  262. indices,
  263. renderer,
  264. batchSize,
  265. currentProgram;
  266. id = 'edges_' + this.conradId;
  267. batchSize = this.settings(options, 'webglEdgesBatchSize');
  268. a = Object.keys(this.edgeFloatArrays);
  269. if (!a.length)
  270. return;
  271. i = 0;
  272. renderer = sigma.webgl.edges[a[i]];
  273. arr = this.edgeFloatArrays[a[i]].array;
  274. indices = this.edgeIndicesArrays[a[i]];
  275. start = 0;
  276. end = Math.min(
  277. start + batchSize * renderer.POINTS,
  278. arr.length / renderer.ATTRIBUTES
  279. );
  280. job = function() {
  281. // Check program:
  282. if (!this.edgePrograms[a[i]])
  283. this.edgePrograms[a[i]] = renderer.initProgram(edgesGl);
  284. if (start < end) {
  285. edgesGl.useProgram(this.edgePrograms[a[i]]);
  286. renderer.render(
  287. edgesGl,
  288. this.edgePrograms[a[i]],
  289. arr,
  290. {
  291. settings: this.settings,
  292. matrix: matrix,
  293. width: this.width,
  294. height: this.height,
  295. ratio: this.camera.ratio,
  296. scalingRatio: this.settings(
  297. options,
  298. 'webglOversamplingRatio'
  299. ),
  300. start: start,
  301. count: end - start,
  302. indicesData: indices
  303. }
  304. );
  305. }
  306. // Catch job's end:
  307. if (
  308. end >= arr.length / renderer.ATTRIBUTES &&
  309. i === a.length - 1
  310. ) {
  311. delete this.jobs[id];
  312. return false;
  313. }
  314. if (end >= arr.length / renderer.ATTRIBUTES) {
  315. i++;
  316. arr = this.edgeFloatArrays[a[i]].array;
  317. renderer = sigma.webgl.edges[a[i]];
  318. start = 0;
  319. end = Math.min(
  320. start + batchSize * renderer.POINTS,
  321. arr.length / renderer.ATTRIBUTES
  322. );
  323. } else {
  324. start = end;
  325. end = Math.min(
  326. start + batchSize * renderer.POINTS,
  327. arr.length / renderer.ATTRIBUTES
  328. );
  329. }
  330. return true;
  331. };
  332. this.jobs[id] = job;
  333. conrad.addJob(id, job.bind(this));
  334. }).call(this);
  335. else {
  336. for (k in this.edgeFloatArrays) {
  337. renderer = sigma.webgl.edges[k];
  338. // Check program:
  339. if (!this.edgePrograms[k])
  340. this.edgePrograms[k] = renderer.initProgram(edgesGl);
  341. // Render
  342. if (this.edgeFloatArrays[k]) {
  343. edgesGl.useProgram(this.edgePrograms[k]);
  344. renderer.render(
  345. edgesGl,
  346. this.edgePrograms[k],
  347. this.edgeFloatArrays[k].array,
  348. {
  349. settings: this.settings,
  350. matrix: matrix,
  351. width: this.width,
  352. height: this.height,
  353. ratio: this.camera.ratio,
  354. scalingRatio: this.settings(options, 'webglOversamplingRatio'),
  355. indicesData: this.edgeIndicesArrays[k]
  356. }
  357. );
  358. }
  359. }
  360. }
  361. }
  362. if (drawNodes) {
  363. // Enable blending:
  364. nodesGl.blendFunc(nodesGl.SRC_ALPHA, nodesGl.ONE_MINUS_SRC_ALPHA);
  365. nodesGl.enable(nodesGl.BLEND);
  366. for (k in this.nodeFloatArrays) {
  367. renderer = sigma.webgl.nodes[k];
  368. // Check program:
  369. if (!this.nodePrograms[k])
  370. this.nodePrograms[k] = renderer.initProgram(nodesGl);
  371. // Render
  372. if (this.nodeFloatArrays[k]) {
  373. nodesGl.useProgram(this.nodePrograms[k]);
  374. renderer.render(
  375. nodesGl,
  376. this.nodePrograms[k],
  377. this.nodeFloatArrays[k].array,
  378. {
  379. settings: this.settings,
  380. matrix: matrix,
  381. width: this.width,
  382. height: this.height,
  383. ratio: this.camera.ratio,
  384. scalingRatio: this.settings(options, 'webglOversamplingRatio')
  385. }
  386. );
  387. }
  388. }
  389. }
  390. if (drawLabels) {
  391. a = this.camera.quadtree.area(
  392. this.camera.getRectangle(this.width, this.height)
  393. );
  394. // Apply camera view to these nodes:
  395. this.camera.applyView(
  396. undefined,
  397. undefined,
  398. {
  399. nodes: a,
  400. edges: [],
  401. width: this.width,
  402. height: this.height
  403. }
  404. );
  405. o = function(key) {
  406. return self.settings({
  407. prefix: self.camera.prefix
  408. }, key);
  409. };
  410. for (i = 0, l = a.length; i < l; i++)
  411. if (!a[i].hidden)
  412. (
  413. sigma.canvas.labels[
  414. a[i].type ||
  415. this.settings(options, 'defaultNodeType')
  416. ] || sigma.canvas.labels.def
  417. )(a[i], this.contexts.labels, o);
  418. }
  419. this.dispatchEvent('render');
  420. return this;
  421. };
  422. /**
  423. * This method creates a DOM element of the specified type, switches its
  424. * position to "absolute", references it to the domElements attribute, and
  425. * finally appends it to the container.
  426. *
  427. * @param {string} tag The label tag.
  428. * @param {string} id The id of the element (to store it in
  429. * "domElements").
  430. * @param {?boolean} webgl Will init the WebGL context if true.
  431. */
  432. sigma.renderers.webgl.prototype.initDOM = function(tag, id, webgl) {
  433. var gl,
  434. dom = document.createElement(tag),
  435. self = this;
  436. dom.style.position = 'absolute';
  437. dom.setAttribute('class', 'sigma-' + id);
  438. this.domElements[id] = dom;
  439. this.container.appendChild(dom);
  440. if (tag.toLowerCase() === 'canvas') {
  441. this.contexts[id] = dom.getContext(webgl ? 'experimental-webgl' : '2d', {
  442. preserveDrawingBuffer: true
  443. });
  444. // Adding webgl context loss listeners
  445. if (webgl) {
  446. dom.addEventListener('webglcontextlost', function(e) {
  447. e.preventDefault();
  448. }, false);
  449. dom.addEventListener('webglcontextrestored', function(e) {
  450. self.render();
  451. }, false);
  452. }
  453. }
  454. };
  455. /**
  456. * This method resizes each DOM elements in the container and stores the new
  457. * dimensions. Then, it renders the graph.
  458. *
  459. * @param {?number} width The new width of the container.
  460. * @param {?number} height The new height of the container.
  461. * @return {sigma.renderers.webgl} Returns the instance itself.
  462. */
  463. sigma.renderers.webgl.prototype.resize = function(w, h) {
  464. var k,
  465. oldWidth = this.width,
  466. oldHeight = this.height,
  467. pixelRatio = sigma.utils.getPixelRatio();
  468. if (w !== undefined && h !== undefined) {
  469. this.width = w;
  470. this.height = h;
  471. } else {
  472. this.width = this.container.offsetWidth;
  473. this.height = this.container.offsetHeight;
  474. w = this.width;
  475. h = this.height;
  476. }
  477. if (oldWidth !== this.width || oldHeight !== this.height) {
  478. for (k in this.domElements) {
  479. this.domElements[k].style.width = w + 'px';
  480. this.domElements[k].style.height = h + 'px';
  481. if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
  482. // If simple 2D canvas:
  483. if (this.contexts[k] && this.contexts[k].scale) {
  484. this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px');
  485. this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px');
  486. if (pixelRatio !== 1)
  487. this.contexts[k].scale(pixelRatio, pixelRatio);
  488. } else {
  489. this.domElements[k].setAttribute(
  490. 'width',
  491. (w * this.settings('webglOversamplingRatio')) + 'px'
  492. );
  493. this.domElements[k].setAttribute(
  494. 'height',
  495. (h * this.settings('webglOversamplingRatio')) + 'px'
  496. );
  497. }
  498. }
  499. }
  500. }
  501. // Scale:
  502. for (k in this.contexts)
  503. if (this.contexts[k] && this.contexts[k].viewport)
  504. this.contexts[k].viewport(
  505. 0,
  506. 0,
  507. this.width * this.settings('webglOversamplingRatio'),
  508. this.height * this.settings('webglOversamplingRatio')
  509. );
  510. return this;
  511. };
  512. /**
  513. * This method clears each canvas.
  514. *
  515. * @return {sigma.renderers.webgl} Returns the instance itself.
  516. */
  517. sigma.renderers.webgl.prototype.clear = function() {
  518. this.contexts.labels.clearRect(0, 0, this.width, this.height);
  519. this.contexts.nodes.clear(this.contexts.nodes.COLOR_BUFFER_BIT);
  520. this.contexts.edges.clear(this.contexts.edges.COLOR_BUFFER_BIT);
  521. return this;
  522. };
  523. /**
  524. * This method kills contexts and other attributes.
  525. */
  526. sigma.renderers.webgl.prototype.kill = function() {
  527. var k,
  528. captor;
  529. // Kill captors:
  530. while ((captor = this.captors.pop()))
  531. captor.kill();
  532. delete this.captors;
  533. // Kill contexts:
  534. for (k in this.domElements) {
  535. this.domElements[k].parentNode.removeChild(this.domElements[k]);
  536. delete this.domElements[k];
  537. delete this.contexts[k];
  538. }
  539. delete this.domElements;
  540. delete this.contexts;
  541. };
  542. /**
  543. * The object "sigma.webgl.nodes" contains the different WebGL node
  544. * renderers. The default one draw nodes as discs. Here are the attributes
  545. * any node renderer must have:
  546. *
  547. * {number} POINTS The number of points required to draw a node.
  548. * {number} ATTRIBUTES The number of attributes needed to draw one point.
  549. * {function} addNode A function that adds a node to the data stack that
  550. * will be given to the buffer. Here is the arguments:
  551. * > {object} node
  552. * > {number} index The node index in the
  553. * nodes array.
  554. * > {Float32Array} data The stack.
  555. * > {object} options Some options.
  556. * {function} render The function that will effectively render the nodes
  557. * into the buffer.
  558. * > {WebGLRenderingContext} gl
  559. * > {WebGLProgram} program
  560. * > {Float32Array} data The stack to give to the
  561. * buffer.
  562. * > {object} params An object containing some
  563. * options, like width,
  564. * height, the camera ratio.
  565. * {function} initProgram The function that will initiate the program, with
  566. * the relevant shaders and parameters. It must return
  567. * the newly created program.
  568. *
  569. * Check sigma.webgl.nodes.def or sigma.webgl.nodes.fast to see how it
  570. * works more precisely.
  571. */
  572. sigma.utils.pkg('sigma.webgl.nodes');
  573. /**
  574. * The object "sigma.webgl.edges" contains the different WebGL edge
  575. * renderers. The default one draw edges as direct lines. Here are the
  576. * attributes any edge renderer must have:
  577. *
  578. * {number} POINTS The number of points required to draw an edge.
  579. * {number} ATTRIBUTES The number of attributes needed to draw one point.
  580. * {function} addEdge A function that adds an edge to the data stack that
  581. * will be given to the buffer. Here is the arguments:
  582. * > {object} edge
  583. * > {object} source
  584. * > {object} target
  585. * > {Float32Array} data The stack.
  586. * > {object} options Some options.
  587. * {function} render The function that will effectively render the edges
  588. * into the buffer.
  589. * > {WebGLRenderingContext} gl
  590. * > {WebGLProgram} program
  591. * > {Float32Array} data The stack to give to the
  592. * buffer.
  593. * > {object} params An object containing some
  594. * options, like width,
  595. * height, the camera ratio.
  596. * {function} initProgram The function that will initiate the program, with
  597. * the relevant shaders and parameters. It must return
  598. * the newly created program.
  599. *
  600. * Check sigma.webgl.edges.def or sigma.webgl.edges.fast to see how it
  601. * works more precisely.
  602. */
  603. sigma.utils.pkg('sigma.webgl.edges');
  604. /**
  605. * The object "sigma.canvas.labels" contains the different
  606. * label renderers for the WebGL renderer. Since displaying texts in WebGL is
  607. * definitely painful and since there a way less labels to display than nodes
  608. * or edges, the default renderer simply renders them in a canvas.
  609. *
  610. * A labels renderer is a simple function, taking as arguments the related
  611. * node, the renderer and a settings function.
  612. */
  613. sigma.utils.pkg('sigma.canvas.labels');
  614. }).call(this);