UNPKG

sigma-next

Version:

A JavaScript library dedicated to graph drawing.

1,688 lines (1,460 loc) 126 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var events = require('events'); var extent = require('graphology-metrics/extent'); var isGraph = _interopDefault(require('graphology-utils/is-graph')); /** * Sigma.js Utils * =============== * * Various helper functions & classes used throughout the library. */ /** * Very simple Object.assign-like function. * * @param {object} target - First object. * @param {object} [...objects] - Objects to merge. * @return {object} */ function assign(target) { target = target || {}; for (var _len = arguments.length, objects = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { objects[_key - 1] = arguments[_key]; } for (var i = 0, l = objects.length; i < l; i++) { if (!objects[i]) continue; for (var k in objects[i]) { target[k] = objects[i][k]; } } return target; } function extend(array, values) { var l1 = array.length; var l2 = values.length; if (l2 === 0) return; array.length += values.length; for (var i = 0; i < l2; i++) { array[l1 + i] = values[i]; } } /** * Sigma.js Easings * ================= * * Handy collection of easing functions. */ var linear = function linear(k) { return k; }; var quadraticIn = function quadraticIn(k) { return k * k; }; var quadraticOut = function quadraticOut(k) { return k * (2 - k); }; var quadraticInOut = function quadraticInOut(k) { k *= 2; if (k < 1) return 0.5 * k * k; return -0.5 * (--k * (k - 2) - 1); }; var cubicIn = function cubicIn(k) { return k * k * k; }; var cubicOut = function cubicOut(k) { return --k * k * k + 1; }; var cubicInOut = function cubicInOut(k) { k *= 2; if (k < 1) return 0.5 * k * k * k; return 0.5 * ((k -= 2) * k * k + 2); }; var easings = /*#__PURE__*/Object.freeze({ linear: linear, quadraticIn: quadraticIn, quadraticOut: quadraticOut, quadraticInOut: quadraticInOut, cubicIn: cubicIn, cubicOut: cubicOut, cubicInOut: cubicInOut }); /** * Sigma.js Animation Helpers * =========================== * * Handy helper functions dealing with nodes & edges attributes animation. */ /** * Defaults. */ var ANIMATE_DEFAULTS = { easing: 'quadraticInOut', duration: 150 }; /** * Function used to animate the nodes. */ function animateNodes(graph, targets, options, callback) { options = assign({}, ANIMATE_DEFAULTS, options); var easing = typeof options.easing === 'function' ? options.easing : easings[options.easing]; var start = Date.now(); var startPositions = {}; for (var node in targets) { var attrs = targets[node]; startPositions[node] = {}; for (var k in attrs) { startPositions[node][k] = graph.getNodeAttribute(node, k); } } var frame = null; var step = function step() { var p = (Date.now() - start) / options.duration; if (p >= 1) { // Animation is done for (var _node in targets) { var _attrs = targets[_node]; for (var _k in _attrs) { graph.setNodeAttribute(_node, _k, _attrs[_k]); } } if (typeof callback === 'function') callback(); return; } p = easing(p); for (var _node2 in targets) { var _attrs2 = targets[_node2]; var s = startPositions[_node2]; for (var _k2 in _attrs2) { graph.setNodeAttribute(_node2, _k2, _attrs2[_k2] * p + s[_k2] * (1 - p)); } } frame = requestAnimationFrame(step); }; step(); return function () { if (frame) cancelAnimationFrame(frame); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } /** * Renderer class. * * @constructor */ var Renderer = /*#__PURE__*/ function (_EventEmitter) { _inherits(Renderer, _EventEmitter); function Renderer() { _classCallCheck(this, Renderer); return _possibleConstructorReturn(this, _getPrototypeOf(Renderer).apply(this, arguments)); } return Renderer; }(events.EventEmitter); /** * Defaults. */ var ANIMATE_DEFAULTS$1 = { easing: 'quadraticInOut', duration: 150 }; var DEFAULT_ZOOMING_RATIO = 1.5; // TODO: animate options = number polymorphism? // TODO: pan, zoom, unzoom, reset, rotate, zoomTo // TODO: add width / height to camera and add #.resize // TODO: bind camera to renderer rather than sigma // TODO: add #.graphToDisplay, #.displayToGraph, batch methods later /** * Camera class * * @constructor */ var Camera = /*#__PURE__*/ function (_EventEmitter) { _inherits(Camera, _EventEmitter); function Camera() { var _this; _classCallCheck(this, Camera); _this = _possibleConstructorReturn(this, _getPrototypeOf(Camera).call(this)); // Properties _this.x = 0.5; _this.y = 0.5; _this.angle = 0; _this.ratio = 1; // State _this.nextFrame = null; _this.previousState = _this.getState(); _this.enabled = true; return _this; } /** * Method used to enable the camera. * * @return {Camera} */ _createClass(Camera, [{ key: "enable", value: function enable() { this.enabled = true; return this; } /** * Method used to disable the camera. * * @return {Camera} */ }, { key: "disable", value: function disable() { this.enabled = false; return this; } /** * Method used to retrieve the camera's current state. * * @return {object} */ }, { key: "getState", value: function getState() { return { x: this.x, y: this.y, angle: this.angle, ratio: this.ratio }; } /** * Method used to retrieve the camera's previous state. * * @return {object} */ }, { key: "getPreviousState", value: function getPreviousState() { var state = this.previousState; return { x: state.x, y: state.y, angle: state.angle, ratio: state.ratio }; } /** * Method used to check whether the camera is currently being animated. * * @return {boolean} */ }, { key: "isAnimated", value: function isAnimated() { return !!this.nextFrame; } /** * Method returning the coordinates of a point from the graph frame to the * viewport. * * @param {object} dimensions - Dimensions of the viewport. * @param {number} x - The X coordinate. * @param {number} y - The Y coordinate. * @return {object} - The point coordinates in the viewport. */ // TODO: assign to gain one object // TODO: angles }, { key: "graphToViewport", value: function graphToViewport(dimensions, x, y) { var smallestDimension = Math.min(dimensions.width, dimensions.height); var dx = smallestDimension / dimensions.width; var dy = smallestDimension / dimensions.height; // TODO: we keep on the upper left corner! // TODO: how to normalize sizes? return { x: (x - this.x + this.ratio / 2 / dx) * (smallestDimension / this.ratio), y: (this.y - y + this.ratio / 2 / dy) * (smallestDimension / this.ratio) }; } /** * Method returning the coordinates of a point from the viewport frame to the * graph frame. * * @param {object} dimensions - Dimensions of the viewport. * @param {number} x - The X coordinate. * @param {number} y - The Y coordinate. * @return {object} - The point coordinates in the graph frame. */ // TODO: angles }, { key: "viewportToGraph", value: function viewportToGraph(dimensions, x, y) { var smallestDimension = Math.min(dimensions.width, dimensions.height); var dx = smallestDimension / dimensions.width; var dy = smallestDimension / dimensions.height; return { x: this.ratio / smallestDimension * x + this.x - this.ratio / 2 / dx, y: -(this.ratio / smallestDimension * y - this.y - this.ratio / 2 / dy) }; } /** * Method returning the abstract rectangle containing the graph according * to the camera's state. * * @return {object} - The view's rectangle. */ // TODO: angle }, { key: "viewRectangle", value: function viewRectangle(dimensions) { // TODO: reduce relative margin? var marginX = 0; // (0 * dimensions.width) / 8; var marginY = 0; // (0 * dimensions.height) / 8; var p1 = this.viewportToGraph(dimensions, 0 - marginX, 0 - marginY); var p2 = this.viewportToGraph(dimensions, dimensions.width + marginX, 0 - marginY); var h = this.viewportToGraph(dimensions, 0, dimensions.height + marginY); return { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, height: p2.y - h.y }; } /** * Method used to set the camera's state. * * @param {object} state - New state. * @return {Camera} */ }, { key: "setState", value: function setState(state) { if (!this.enabled) return this; // TODO: validations // TODO: update by function // Keeping track of last state this.previousState = this.getState(); if ('x' in state) this.x = state.x; if ('y' in state) this.y = state.y; if ('angle' in state) this.angle = state.angle; if ('ratio' in state) this.ratio = state.ratio; // Emitting // TODO: don't emit if nothing changed? this.emit('updated', this.getState()); return this; } /** * Method used to animate the camera. * * @param {object} state - State to reach eventually. * @param {object} options - Options: * @param {number} duration - Duration of the animation. * @param {function} callback - Callback * @return {function} - Return a function to cancel the animation. */ }, { key: "animate", value: function animate(state, options, callback) { var _this2 = this; if (!this.enabled) return; // TODO: validation options = assign({}, ANIMATE_DEFAULTS$1, options); var easing = typeof options.easing === 'function' ? options.easing : easings[options.easing]; // Canceling previous animation if needed if (this.nextFrame) cancelAnimationFrame(this.nextFrame); // State var start = Date.now(); var initialState = this.getState(); // Function performing the animation var fn = function fn() { var t = (Date.now() - start) / options.duration; // The animation is over: if (t >= 1) { _this2.nextFrame = null; _this2.setState(state); if (typeof callback === 'function') callback(); return; } var coefficient = easing(t); var newState = {}; if ('x' in state) newState.x = initialState.x + (state.x - initialState.x) * coefficient; if ('y' in state) newState.y = initialState.y + (state.y - initialState.y) * coefficient; if ('angle' in state) newState.angle = initialState.angle + (state.angle - initialState.angle) * coefficient; if ('ratio' in state) newState.ratio = initialState.ratio + (state.ratio - initialState.ratio) * coefficient; _this2.setState(newState); _this2.nextFrame = requestAnimationFrame(fn); }; if (this.nextFrame) { cancelAnimationFrame(this.nextFrame); this.nextFrame = requestAnimationFrame(fn); } else { fn(); } } /** * Method used to zoom the camera. * * @param {number|object} factorOrOptions - Factor or options. * @return {function} */ }, { key: "animatedZoom", value: function animatedZoom(factorOrOptions) { if (!factorOrOptions) { return this.animate({ ratio: this.ratio / DEFAULT_ZOOMING_RATIO }); } else if (typeof factorOrOptions === 'number') return this.animate({ ratio: this.ratio / factorOrOptions });else return this.animate({ ratio: this.ratio / (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO) }, factorOrOptions); } /** * Method used to unzoom the camera. * * @param {number|object} factorOrOptions - Factor or options. * @return {function} */ }, { key: "animatedUnzoom", value: function animatedUnzoom(factorOrOptions) { if (!factorOrOptions) { return this.animate({ ratio: this.ratio * DEFAULT_ZOOMING_RATIO }); } else if (typeof factorOrOptions === 'number') return this.animate({ ratio: this.ratio * factorOrOptions });else return this.animate({ ratio: this.ratio * (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO) }, factorOrOptions); } /** * Method used to reset the camera. * * @param {object} options - Options. * @return {function} */ }, { key: "animatedReset", value: function animatedReset(options) { return this.animate({ x: 0.5, y: 0.5, ratio: 1, angle: 0 }, options); } }]); return Camera; }(events.EventEmitter); // sight. // TODO: a square can be represented as topleft + width, saying for the quad blocks (reduce mem) // TODO: jsdoc // TODO: be sure we can handle cases overcoming boundaries (because of size) or use a maxed size // TODO: filtering unwanted labels beforehand through the filter function // NOTE: this is basically a MX-CIF Quadtree at this point // NOTE: need to explore R-Trees for edges // NOTE: need to explore 2d segment tree for edges // NOTE: probably can do faster using spatial hashing /** * Constants. * * Note that since we are representing a static 4-ary tree, the indices of the * quadrants are the following: * - TOP_LEFT: 4i + b * - TOP_RIGHT: 4i + 2b * - BOTTOM_LEFT: 4i + 3b * - BOTTOM_RIGHT: 4i + 4b */ var BLOCKS = 4; var MAX_LEVEL = 5; var X_OFFSET = 0; var Y_OFFSET = 1; var WIDTH_OFFSET = 2; var HEIGHT_OFFSET = 3; var TOP_LEFT = 1; var TOP_RIGHT = 2; var BOTTOM_LEFT = 3; var BOTTOM_RIGHT = 4; /** * Geometry helpers. */ /** * Function returning whether the given rectangle is axis-aligned. * * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @return {boolean} */ function isAxisAligned(x1, y1, x2, y2) { return x1 === x2 || y1 === y2; } function squareCollidesWithQuad(x1, y1, w, qx, qy, qw, qh) { return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + w > qy; } function rectangleCollidesWithQuad(x1, y1, w, h, qx, qy, qw, qh) { return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + h > qy; } function pointIsInQuad(x, y, qx, qy, qw, qh) { var xmp = qx + qw / 2; var ymp = qy + qh / 2; var top = y < ymp; var left = x < xmp; return top ? left ? TOP_LEFT : TOP_RIGHT : left ? BOTTOM_LEFT : BOTTOM_RIGHT; } /** * Helper functions that are not bound to the class so an external user * cannot mess with them. */ function buildQuadrants(maxLevel, data) { // [block, level] var stack = [0, 0]; while (stack.length) { var level = stack.pop(); var block = stack.pop(); var topLeftBlock = 4 * block + BLOCKS; var topRightBlock = 4 * block + 2 * BLOCKS; var bottomLeftBlock = 4 * block + 3 * BLOCKS; var bottomRightBlock = 4 * block + 4 * BLOCKS; var x = data[block + X_OFFSET]; var y = data[block + Y_OFFSET]; var width = data[block + WIDTH_OFFSET]; var height = data[block + HEIGHT_OFFSET]; var hw = width / 2; var hh = height / 2; data[topLeftBlock + X_OFFSET] = x; data[topLeftBlock + Y_OFFSET] = y; data[topLeftBlock + WIDTH_OFFSET] = hw; data[topLeftBlock + HEIGHT_OFFSET] = hh; data[topRightBlock + X_OFFSET] = x + hw; data[topRightBlock + Y_OFFSET] = y; data[topRightBlock + WIDTH_OFFSET] = hw; data[topRightBlock + HEIGHT_OFFSET] = hh; data[bottomLeftBlock + X_OFFSET] = x; data[bottomLeftBlock + Y_OFFSET] = y + hh; data[bottomLeftBlock + WIDTH_OFFSET] = hw; data[bottomLeftBlock + HEIGHT_OFFSET] = hh; data[bottomRightBlock + X_OFFSET] = x + hw; data[bottomRightBlock + Y_OFFSET] = y + hh; data[bottomRightBlock + WIDTH_OFFSET] = hw; data[bottomRightBlock + HEIGHT_OFFSET] = hh; if (level < maxLevel - 1) { stack.push(bottomRightBlock, level + 1); stack.push(bottomLeftBlock, level + 1); stack.push(topRightBlock, level + 1); stack.push(topLeftBlock, level + 1); } } } function insertNode(maxLevel, data, containers, key, x, y, size) { var x1 = x - size; var y1 = y - size; var w = size * 2; var level = 0; var block = 0; while (true) { // If we reached max level if (level >= maxLevel) { containers[block] = containers[block] || []; containers[block].push(key); return; } var topLeftBlock = 4 * block + BLOCKS; var topRightBlock = 4 * block + 2 * BLOCKS; var bottomLeftBlock = 4 * block + 3 * BLOCKS; var bottomRightBlock = 4 * block + 4 * BLOCKS; var collidingWithTopLeft = squareCollidesWithQuad(x1, y1, w, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]); var collidingWithTopRight = squareCollidesWithQuad(x1, y1, w, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]); var collidingWithBottomLeft = squareCollidesWithQuad(x1, y1, w, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]); var collidingWithBottomRight = squareCollidesWithQuad(x1, y1, w, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]); var collisions = collidingWithTopLeft + collidingWithTopRight + collidingWithBottomLeft + collidingWithBottomRight; // If we don't have at least a collision, there is an issue if (collisions === 0) throw new Error("sigma/quadtree.insertNode: no collision (level: ".concat(level, ", key: ").concat(key, ", x: ").concat(x, ", y: ").concat(y, ", size: ").concat(size, ").")); // If we have 3 collisions, we have a geometry problem obviously if (collisions === 3) throw new Error("sigma/quadtree.insertNode: 3 impossible collisions (level: ".concat(level, ", key: ").concat(key, ", x: ").concat(x, ", y: ").concat(y, ", size: ").concat(size, ").")); // If we have more that one collision, we stop here and store the node // in the relevant containers if (collisions > 1) { // NOTE: this is a nice way to optimize for hover, but not for frustum // since it requires to uniq the collected nodes // if (collisions < 4) { // // If we intersect two quads, we place the node in those two // if (collidingWithTopLeft) { // containers[topLeftBlock] = containers[topLeftBlock] || []; // containers[topLeftBlock].push(key); // } // if (collidingWithTopRight) { // containers[topRightBlock] = containers[topRightBlock] || []; // containers[topRightBlock].push(key); // } // if (collidingWithBottomLeft) { // containers[bottomLeftBlock] = containers[bottomLeftBlock] || []; // containers[bottomLeftBlock].push(key); // } // if (collidingWithBottomRight) { // containers[bottomRightBlock] = containers[bottomRightBlock] || []; // containers[bottomRightBlock].push(key); // } // } // else { // // Else we keep the node where it is to avoid more pointless computations // containers[block] = containers[block] || []; // containers[block].push(key); // } containers[block] = containers[block] || []; containers[block].push(key); return; } else { level++; } // Else we recurse into the correct quads if (collidingWithTopLeft) block = topLeftBlock; if (collidingWithTopRight) block = topRightBlock; if (collidingWithBottomLeft) block = bottomLeftBlock; if (collidingWithBottomRight) block = bottomRightBlock; } } function getNodesInAxisAlignedRectangleArea(maxLevel, data, containers, x1, y1, w, h) { // [block, level] var stack = [0, 0]; var collectedNodes = []; var container; while (stack.length) { var level = stack.pop(); var block = stack.pop(); // Collecting nodes container = containers[block]; if (container) extend(collectedNodes, container); // If we reached max level if (level >= maxLevel) continue; var topLeftBlock = 4 * block + BLOCKS; var topRightBlock = 4 * block + 2 * BLOCKS; var bottomLeftBlock = 4 * block + 3 * BLOCKS; var bottomRightBlock = 4 * block + 4 * BLOCKS; var collidingWithTopLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]); var collidingWithTopRight = rectangleCollidesWithQuad(x1, y1, w, h, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]); var collidingWithBottomLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]); var collidingWithBottomRight = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]); if (collidingWithTopLeft) stack.push(topLeftBlock, level + 1); if (collidingWithTopRight) stack.push(topRightBlock, level + 1); if (collidingWithBottomLeft) stack.push(bottomLeftBlock, level + 1); if (collidingWithBottomRight) stack.push(bottomRightBlock, level + 1); } return collectedNodes; } /** * QuadTree class. * * @constructor * @param {object} boundaries - The graph boundaries. */ var QuadTree = /*#__PURE__*/ function () { function QuadTree(params) { _classCallCheck(this, QuadTree); params = params || {}; // Allocating the underlying byte array var L = Math.pow(4, MAX_LEVEL); this.data = new Float32Array(BLOCKS * ((4 * L - 1) / 3)); this.containers = {}; this.cache = null; this.lastRectangle = null; if (params.boundaries) this.resize(params.boundaries);else this.resize({ x: 0, y: 0, width: 1, height: 1 }); if (typeof params.filter === 'function') this.nodeFilter = params.filter; } _createClass(QuadTree, [{ key: "add", value: function add(key, x, y, size) { insertNode(MAX_LEVEL, this.data, this.containers, key, x, y, size); return this; } }, { key: "resize", value: function resize(boundaries) { this.clear(); // Building the quadrants this.data[X_OFFSET] = boundaries.x; this.data[Y_OFFSET] = boundaries.y; this.data[WIDTH_OFFSET] = boundaries.width; this.data[HEIGHT_OFFSET] = boundaries.height; buildQuadrants(MAX_LEVEL, this.data); } }, { key: "clear", value: function clear() { this.containers = {}; return this; } }, { key: "point", value: function point(x, y) { var nodes = []; var block = 0; var level = 0; do { if (this.containers[block]) nodes.push.apply(nodes, _toConsumableArray(this.containers[block])); var quad = pointIsInQuad(x, y, this.data[block + X_OFFSET], this.data[block + Y_OFFSET], this.data[block + WIDTH_OFFSET], this.data[block + HEIGHT_OFFSET]); block = 4 * block + quad * BLOCKS; level++; } while (level <= MAX_LEVEL); return nodes; } }, { key: "rectangle", value: function rectangle(x1, y1, x2, y2, height) { var lr = this.lastRectangle; if (lr && x1 === lr.x1 && x2 === lr.x2 && y1 === lr.y1 && y2 === lr.y2 && height === lr.height) { return this.cache; } this.lastRectangle = { x1: x1, y1: y1, x2: x2, y2: y2, height: height }; // Is the rectangle axis aligned? if (!isAxisAligned(x1, y1, x2, y2)) throw new Error('sigma/quadtree.rectangle: shifted view is not yet implemented.'); var collectedNodes = getNodesInAxisAlignedRectangleArea(MAX_LEVEL, this.data, this.containers, x1, y1, Math.abs(x1 - x2) || Math.abs(y1 - y2), height); this.cache = collectedNodes; return this.cache; } }]); return QuadTree; }(); var Captor = /*#__PURE__*/ function (_EventEmitter) { _inherits(Captor, _EventEmitter); function Captor(container, camera) { var _this; _classCallCheck(this, Captor); _this = _possibleConstructorReturn(this, _getPrototypeOf(Captor).call(this)); // Properties _this.container = container; _this.camera = camera; return _this; } return Captor; }(events.EventEmitter); /** * Sigma.js Rendering Utils * =========================== * * Helpers used by most renderers. */ /** * Function used to create DOM elements easily. * * @param {string} tag - Tag name of the element to create. * @param {object} attributes - Attributes map. * @return {HTMLElement} */ function createElement(tag, attributes) { var element = document.createElement(tag); if (!attributes) return element; for (var k in attributes) { if (k === 'style') { for (var s in attributes[k]) { element.style[s] = attributes[k][s]; } } else { element.setAttribute(k, attributes[k]); } } return element; } /** * Function returning the browser's pixel ratio. * * @return {number} */ function getPixelRatio() { var _window = window, screen = _window.screen; if (typeof screen.deviceXDPI !== 'undefined' && typeof screen.logicalXDPI !== 'undefined' && screen.deviceXDPI > screen.logicalXDPI) return screen.systemXDPI / screen.logicalXDPI;else if (typeof window.devicePixelRatio !== 'undefined') return window.devicePixelRatio; return 1; } /** * Factory returning a function normalizing the given node's position & size. * * @param {object} extent - Extent of the graph. * @return {function} */ function createNormalizationFunction(extent) { var _extent$x = _slicedToArray(extent.x, 2), minX = _extent$x[0], maxX = _extent$x[1], _extent$y = _slicedToArray(extent.y, 2), minY = _extent$y[0], maxY = _extent$y[1]; var ratio = Math.max(maxX - minX, maxY - minY); if (ratio === 0) ratio = 1; var dX = (maxX + minX) / 2; var dY = (maxY + minY) / 2; var fn = function fn(data) { return { x: 0.5 + (data.x - dX) / ratio, y: 0.5 + (data.y - dY) / ratio }; }; // TODO: possibility to apply this in batch over array of indices fn.applyTo = function (data) { data.x = 0.5 + (data.x - dX) / ratio; data.y = 0.5 + (data.y - dY) / ratio; }; fn.inverse = function (data) { return { x: dX + ratio * (data.x - 0.5), y: dY + ratio * (data.y - 0.5) }; }; fn.ratio = ratio; return fn; } /** * Sigma.js Captor Utils * ====================== * * Miscellenous helper functions related to the captors. */ /** * Extract the local X position from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {number} The local X value of the mouse. */ function getX(e) { if (typeof e.offsetX !== 'undefined') return e.offsetX; if (typeof e.layerX !== 'undefined') return e.layerX; if (typeof e.clientX !== 'undefined') return e.clientX; throw new Error('sigma/captors/utils.getX: could not extract x from event.'); } /** * Extract the local Y position from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {number} The local Y value of the mouse. */ function getY(e) { if (typeof e.offsetY !== 'undefined') return e.offsetY; if (typeof e.layerY !== 'undefined') return e.layerY; if (typeof e.clientY !== 'undefined') return e.clientY; throw new Error('sigma/captors/utils.getY: could not extract y from event.'); } /** * Extract the width from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {number} The width of the event's target. */ function getWidth(e) { var w = !e.target.ownerSVGElement ? e.target.width : e.target.ownerSVGElement.width; if (typeof w === 'number') return w; if (w !== undefined && w.baseVal !== undefined) return w.baseVal.value; throw new Error('sigma/captors/utils.getWidth: could not extract width from event.'); } /** * Extract the height from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {number} The height of the event's target. */ function getHeight(e) { var w = !e.target.ownerSVGElement ? e.target.height : e.target.ownerSVGElement.height; if (typeof w === 'number') return w; if (w !== undefined && w.baseVal !== undefined) return w.baseVal.value; throw new Error('sigma/captors/utils.getHeight: could not extract height from event.'); } /** * Extract the center from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {object} The center of the event's target. */ function getCenter(e) { var ratio = e.target.namespaceURI.indexOf('svg') !== -1 ? 1 : getPixelRatio(); return { x: getWidth(e) / (2 * ratio), y: getHeight(e) / (2 * ratio) }; } /** * Convert mouse coords to sigma coords. * * @param {event} e - A mouse or touch event. * @param {number} [x] - The x coord to convert * @param {number} [y] - The y coord to convert * * @return {object} */ function getMouseCoords(e) { return { x: getX(e), y: getY(e), clientX: e.clientX, clientY: e.clientY, ctrlKey: e.ctrlKey, metaKey: e.metaKey, altKey: e.altKey, shiftKey: e.shiftKey }; } /** * Extract the wheel delta from a mouse or touch event. * * @param {event} e - A mouse or touch event. * @return {number} The wheel delta of the mouse. */ function getWheelDelta(e) { if (typeof e.wheelDelta !== 'undefined') return e.wheelDelta / 360; if (typeof e.detail !== 'undefined') return e.detail / -9; throw new Error('sigma/captors/utils.getDelta: could not extract delta from event.'); } /** * Constants. */ var DRAG_TIMEOUT = 200; var MOUSE_INERTIA_DURATION = 200; var MOUSE_INERTIA_RATIO = 3; var MOUSE_ZOOM_DURATION = 200; var ZOOMING_RATIO = 1.7; var DOUBLE_CLICK_TIMEOUT = 300; var DOUBLE_CLICK_ZOOMING_RATIO = 2.2; var DOUBLE_CLICK_ZOOMING_DURATION = 200; /** * Mouse captor class. * * @constructor */ var MouseCaptor = /*#__PURE__*/ function (_Captor) { _inherits(MouseCaptor, _Captor); function MouseCaptor(container, camera) { var _this; _classCallCheck(this, MouseCaptor); _this = _possibleConstructorReturn(this, _getPrototypeOf(MouseCaptor).call(this, container, camera)); // Properties _this.container = container; _this.camera = camera; // State _this.enabled = true; _this.hasDragged = false; _this.downStartTime = null; _this.lastMouseX = null; _this.lastMouseY = null; _this.isMouseDown = false; _this.isMoving = false; _this.movingTimeout = null; _this.startCameraState = null; _this.lastCameraState = null; _this.clicks = 0; _this.doubleClickTimeout = null; _this.wheelLock = false; // Binding methods _this.handleClick = _this.handleClick.bind(_assertThisInitialized(_this)); _this.handleDown = _this.handleDown.bind(_assertThisInitialized(_this)); _this.handleUp = _this.handleUp.bind(_assertThisInitialized(_this)); _this.handleMove = _this.handleMove.bind(_assertThisInitialized(_this)); _this.handleWheel = _this.handleWheel.bind(_assertThisInitialized(_this)); _this.handleOut = _this.handleOut.bind(_assertThisInitialized(_this)); // Binding events container.addEventListener('click', _this.handleClick, false); container.addEventListener('mousedown', _this.handleDown, false); container.addEventListener('mousemove', _this.handleMove, false); container.addEventListener('DOMMouseScroll', _this.handleWheel, false); container.addEventListener('mousewheel', _this.handleWheel, false); container.addEventListener('mouseout', _this.handleOut, false); document.addEventListener('mouseup', _this.handleUp, false); return _this; } _createClass(MouseCaptor, [{ key: "kill", value: function kill() { var container = this.container; container.removeEventListener('click', this.handleClick); container.removeEventListener('mousedown', this.handleDown); container.removeEventListener('mousemove', this.handleMove); container.removeEventListener('DOMMouseScroll', this.handleWheel); container.removeEventListener('mousewheel', this.handleWheel); container.removeEventListener('mouseout', this.handleOut); document.removeEventListener('mouseup', this.handleUp); } }, { key: "handleClick", value: function handleClick(e) { var _this2 = this; if (!this.enabled) return; this.clicks++; if (this.clicks === 2) { this.clicks = 0; clearTimeout(this.doubleClickTimeout); this.doubleClickTimeout = null; return this.handleDoubleClick(e); } setTimeout(function () { _this2.clicks = 0; _this2.doubleClickTimeout = null; }, DOUBLE_CLICK_TIMEOUT); // NOTE: this is here to prevent click events on drag if (!this.hasDragged) this.emit('click', getMouseCoords(e)); } }, { key: "handleDoubleClick", value: function handleDoubleClick(e) { if (!this.enabled) return; var center = getCenter(e); var cameraState = this.camera.getState(); var newRatio = cameraState.ratio / DOUBLE_CLICK_ZOOMING_RATIO; // TODO: factorize var dimensions = { width: this.container.offsetWidth, height: this.container.offsetHeight }; var clickX = getX(e); var clickY = getY(e); // TODO: baaaad we mustn't mutate the camera, create a Camera.from or #.copy // TODO: factorize pan & zoomTo var cameraWithNewRatio = new Camera(); cameraWithNewRatio.ratio = newRatio; cameraWithNewRatio.x = cameraState.x; cameraWithNewRatio.y = cameraState.y; var clickGraph = this.camera.viewportToGraph(dimensions, clickX, clickY); var centerGraph = this.camera.viewportToGraph(dimensions, center.x, center.y); var clickGraphNew = cameraWithNewRatio.viewportToGraph(dimensions, clickX, clickY); var centerGraphNew = cameraWithNewRatio.viewportToGraph(dimensions, center.x, center.y); var deltaX = clickGraphNew.x - centerGraphNew.x - clickGraph.x + centerGraph.x; var deltaY = clickGraphNew.y - centerGraphNew.y - clickGraph.y + centerGraph.y; this.camera.animate({ x: cameraState.x - deltaX, y: cameraState.y - deltaY, ratio: newRatio }, { easing: 'quadraticInOut', duration: DOUBLE_CLICK_ZOOMING_DURATION }); if (e.preventDefault) e.preventDefault();else e.returnValue = false; e.stopPropagation(); return false; } }, { key: "handleDown", value: function handleDown(e) { if (!this.enabled) return; this.startCameraState = this.camera.getState(); this.lastCameraState = this.startCameraState; this.lastMouseX = getX(e); this.lastMouseY = getY(e); this.hasDragged = false; this.downStartTime = Date.now(); // TODO: dispatch events switch (e.which) { default: // Left button pressed this.isMouseDown = true; this.emit('mousedown', getMouseCoords(e)); } } }, { key: "handleUp", value: function handleUp(e) { var _this3 = this; if (!this.enabled || !this.isMouseDown) return; this.isMouseDown = false; if (this.movingTimeout) { this.movingTimeout = null; clearTimeout(this.movingTimeout); } var x = getX(e); var y = getY(e); var cameraState = this.camera.getState(); var previousCameraState = this.camera.getPreviousState(); if (this.isMoving) { this.camera.animate({ x: cameraState.x + MOUSE_INERTIA_RATIO * (cameraState.x - previousCameraState.x), y: cameraState.y + MOUSE_INERTIA_RATIO * (cameraState.y - previousCameraState.y) }, { duration: MOUSE_INERTIA_DURATION, easing: 'quadraticOut' }); } else if (this.lastMouseX !== x || this.lastMouseY !== y) { this.camera.setState({ x: cameraState.x, y: cameraState.y }); } this.isMoving = false; setImmediate(function () { return _this3.hasDragged = false; }); this.emit('mouseup', getMouseCoords(e)); } }, { key: "handleMove", value: function handleMove(e) { var _this4 = this; if (!this.enabled) return; this.emit('mousemove', getMouseCoords(e)); if (this.isMouseDown) { // TODO: dispatch events this.isMoving = true; this.hasDragged = true; if (this.movingTimeout) clearTimeout(this.movingTimeout); this.movingTimeout = setTimeout(function () { _this4.movingTimeout = null; _this4.isMoving = false; }, DRAG_TIMEOUT); var dimensions = { width: this.container.offsetWidth, height: this.container.offsetHeight }; var eX = getX(e); var eY = getY(e); var lastMouse = this.camera.viewportToGraph(dimensions, this.lastMouseX, this.lastMouseY); var mouse = this.camera.viewportToGraph(dimensions, eX, eY); var offsetX = lastMouse.x - mouse.x; var offsetY = lastMouse.y - mouse.y; var cameraState = this.camera.getState(); var x = cameraState.x + offsetX; var y = cameraState.y + offsetY; this.camera.setState({ x: x, y: y }); this.lastMouseX = eX; this.lastMouseY = eY; } if (e.preventDefault) e.preventDefault();else e.returnValue = false; e.stopPropagation(); return false; } }, { key: "handleWheel", value: function handleWheel(e) { var _this5 = this; if (e.preventDefault) e.preventDefault();else e.returnValue = false; e.stopPropagation(); if (!this.enabled) return false; var delta = getWheelDelta(e); if (!delta) return false; if (this.wheelLock) return false; this.wheelLock = true; // TODO: handle max zoom var ratio = delta > 0 ? 1 / ZOOMING_RATIO : ZOOMING_RATIO; var cameraState = this.camera.getState(); var newRatio = ratio * cameraState.ratio; var center = getCenter(e); var dimensions = { width: this.container.offsetWidth, height: this.container.offsetHeight }; var clickX = getX(e); var clickY = getY(e); // TODO: baaaad we mustn't mutate the camera, create a Camera.from or #.copy // TODO: factorize pan & zoomTo var cameraWithNewRatio = new Camera(); cameraWithNewRatio.ratio = newRatio; cameraWithNewRatio.x = cameraState.x; cameraWithNewRatio.y = cameraState.y; var clickGraph = this.camera.viewportToGraph(dimensions, clickX, clickY); var centerGraph = this.camera.viewportToGraph(dimensions, center.x, center.y); var clickGraphNew = cameraWithNewRatio.viewportToGraph(dimensions, clickX, clickY); var centerGraphNew = cameraWithNewRatio.viewportToGraph(dimensions, center.x, center.y); var deltaX = clickGraphNew.x - centerGraphNew.x - clickGraph.x + centerGraph.x; var deltaY = clickGraphNew.y - centerGraphNew.y - clickGraph.y + centerGraph.y; this.camera.animate({ x: cameraState.x - deltaX, y: cameraState.y - deltaY, ratio: newRatio }, { easing: 'linear', duration: MOUSE_ZOOM_DURATION }, function () { return _this5.wheelLock = false; }); return false; } }, { key: "handleOut", value: function handleOut() {// TODO: dispatch event } }]); return MouseCaptor; }(Captor); /** * Sigma.js Display Data Classes * ============================== * * Classes representing nodes & edges display data aiming at facilitating * the engine's memory representation and keep them in a pool to avoid * requiring to allocate memory too often. * * NOTE: it's possible to optimize this further by maintaining display data * in byte arrays but this would prove more tedious for the rendering logic * afterwards. */ var NodeDisplayData = /*#__PURE__*/ function () { function NodeDisplayData(index, settings) { _classCallCheck(this, NodeDisplayData); this.index = index; this.x = 0; this.y = 0; this.size = 2; this.color = settings.defaultNodeColor; this.hidden = false; this.label = ''; } _createClass(NodeDisplayData, [{ key: "assign", value: function assign(data) { if ('x' in data) this.x = data.x; if ('y' in data) this.y = data.y; if ('size' in data) this.size = data.size; if ('color' in data) this.color = data.color; if ('hidden' in data) this.hidden = data.hidden; if ('label' in data) this.label = data.label; } }]); return NodeDisplayData; }(); var EdgeDisplayData = /*#__PURE__*/ function () { function EdgeDisplayData(index, settings) { _classCallCheck(this, EdgeDisplayData); this.index = index; this.size = 1; this.color = settings.defaultEdgeColor; this.hidden = false; } _createClass(EdgeDisplayData, [{ key: "assign", value: function assign(data) { if ('size' in data) this.size = data.size; if ('color' in data) this.color = data.color; if ('hidden' in data) this.hidden = data.hidden; } }]); return EdgeDisplayData; }(); /** * Sigma.js Shader Utils * ====================== * * Code used to load sigma's shaders. */ /** * Function used to load a shader. */ function loadShader(type, gl, source) { var glType = type === 'VERTEX' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER; // Creating the shader var shader = gl.createShader(glType); // Loading source gl.shaderSource(shader, source); // Compiling the shader gl.compileShader(shader); // Retrieving compilation status var successfullyCompiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); // Throwing if something went awry if (!successfullyCompiled) { var infoLog = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Error("sigma/renderers/webgl/shaders/utils.loadShader: error while compiling the shader:\n".concat(infoLog, "\n").concat(source)); } return shader; } var loadVertexShader = loadShader.bind(null, 'VERTEX'); var loadFragmentShader = loadShader.bind(null, 'FRAGMENT'); /** * Function used to load a program. */ function loadProgram(gl, shaders) { var program = gl.createProgram(); var i; var l; // Attaching the shaders for (i = 0, l = shaders.length; i < l; i++) { gl.attachShader(program, shaders[i]); } gl.linkProgram(program); // Checking status var successfullyLinked = gl.getProgramParameter(program, gl.LINK_STATUS); if (!successfullyLinked) { gl.deleteProgram(program); throw new Error('sigma/renderers/webgl/shaders/utils.loadProgram: error while linking the program.'); } return program; } /** * Program class. * * @constructor */ var Program = /*#__PURE__*/ function () { function Program(gl, vertexShaderSource, fragmentShaderSource) { _classCallCheck(this, Program); this.vertexShaderSource = vertexShaderSource; this.fragmentShaderSource = fragmentShaderSource; this.load(gl); } /** * Method used to load the program into a webgl context. * * @param {WebGLContext} gl - The WebGL context. * @return {WebGLProgram} */ _createClass(Program, [{ key: "load", value: function load(gl) { this.vertexShader = loadVertexShader(gl, this.vertexShaderSource); this.fragmentShader = loadFragmentShader(gl, this.fragmentShaderSource); this.program = loadProgram(gl, [this.vertexShader, this.fragmentShader]); return this.program; } }]); return Program; }(); function createCompoundProgram(programClasses) { return ( /*#__PURE__*/ function () { function CompoundProgram(gl) { _classCallCheck(this, CompoundProgram); this.programs = programClasses.map(function (ProgramClass) { return new ProgramClass(gl); }); } _createClass(CompoundProgram, [{ key: "allocate", value: function allocate(capacity) { this.programs.forEach(function (program) { return program.allocate(capacity); }); } }, { key: "process", value: function process() { // eslint-disable-next-line prefer-rest-params var args = arguments; this.programs.forEach(function (program) { return program.process.apply(program, _toConsumable