module import Graph , { Attributes } from "graphology-types" ; import isGraph from "graphology-utils/is-graph" ; import { CameraState , Coordinates , Dimensions , Extent , PlainObject } from "../types" ; import { multiply , identity , scale , rotate , translate , multiplyVec } from "./matrices" ; import { HTML_COLORS } from "./data" ; param { mixed } value return { boolean } export function isPlainObject ( value ) { return typeof value === "object" && value !== null && value . constructor === Object ; } param { object } target param { object } . . objects return { object } export function assign < T > ( target , ... objects ) { target = target || { } ; for ( let i = 0 , l = objects . length ; i < l ; i ++ ) { const o = objects [ i ] ; if ( ! o ) continue ; Object . assign ( target , o ) ; } return target as T ; } param { object } target param { object } . . objects return { object } export function assignDeep < T > ( target , ... objects ) { target = target || { } ; for ( let i = 0 , l = objects . length ; i < l ; i ++ ) { const o = objects [ i ] ; if ( ! o ) continue ; for ( const k in o ) { if ( isPlainObject ( o [ k ] ) ) { target [ k ] = assignDeep ( target [ k ] , o [ k ] ) ; } else { target [ k ] = o [ k ] ; } } } return target as T ; } /**
 * Just some dirty trick to make requestAnimationFrame and cancelAnimationFrame "work" in Node.js, for unit tests:
 */ export const requestFrame = typeof requestAnimationFrame !== "undefined" ? ( callback ) => requestAnimationFrame ( callback ) : ( callback ) => setTimeout ( callback , 0 ) ; export const cancelFrame = typeof cancelAnimationFrame !== "undefined" ? ( requestID ) => cancelAnimationFrame ( requestID ) : ( requestID ) => clearTimeout ( requestID ) ; param { string } tag param { object } style param { object } attributes return { HTMLElement } export function createElement < T extends HTMLElement > ( tag , style ? , attributes ? ) { const element = document . createElement ( tag ) as T ; if ( style ) { for ( const k in style ) { element . style [ k ] = style [ k ] as string ; } } if ( attributes ) { for ( const k in attributes ) { element . setAttribute ( k , attributes [ k ] ) ; } } return element ; } return { number } export function getPixelRatio ( ) { if ( typeof window . devicePixelRatio !== "undefined" ) return window . devicePixelRatio ; return 1 ; } param { Graph } return { object } export function graphExtent ( graph ) { let xMin = Infinity ; let xMax = - Infinity ; let yMin = Infinity ; let yMax = - Infinity ; graph . forEachNode ( ( _ , attr ) => { const { x , y } = attr ; if ( x < xMin ) xMin = x ; if ( x > xMax ) xMax = x ; if ( y < yMin ) yMin = y ; if ( y > yMax ) yMax = y ; } ) ; return { x : [ xMin , xMax ] , y : [ yMin , yMax ] } ; } param { object } extent return { function } export interface NormalizationFunction { ( data ) : Coordinates ; ratio : number ; inverse ( data ) : Coordinates ; applyTo ( data ) : void ; } export function createNormalizationFunction ( extent ) { const { x : [ minX , maxX ] , y : [ minY , maxY ] , } = extent ; let ratio = Math . max ( maxX - minX , maxY - minY ) ; if ( ratio === 0 ) ratio = 1 ; const dX = ( maxX + minX ) / 2 , dY = ( maxY + minY ) / 2 ; const fn = ( data ) : Coordinates => { return { x : 0.5 + ( data . x - dX ) / ratio , y : 0.5 + ( data . y - dY ) / ratio , } ; } ; fn . applyTo = ( data ) : void => { data . x = 0.5 + ( data . x - dX ) / ratio ; data . y = 0.5 + ( data . y - dY ) / ratio ; } ; fn . inverse = ( data ) : Coordinates => { return { x : dX + ratio * ( data . x - 0.5 ) , y : dY + ratio * ( data . y - 0.5 ) , } ; } ; fn . ratio = ratio ; return fn ; } param { number } extent param { function } getter param { array } elements return { array } export function zIndexOrdering < T > ( extent , getter , elements ) { return elements . sort ( function ( a , b ) { const zA = getter ( a ) || 0 , zB = getter ( b ) || 0 ; if ( zA < zB ) return - 1 ; if ( zA > zB ) return 1 ; return 0 ; } ) ; } /**
 * WebGL utils
 * ===========
 */ /**
 * Memoized function returning a float-encoded color from various string
 * formats describing colors.
 */ const INT8 = new Int8Array ( 4 ) ; const INT32 = new Int32Array ( INT8 . buffer , 0 , 1 ) ; const FLOAT32 = new Float32Array ( INT8 . buffer , 0 , 1 ) ; const RGBA_TEST_REGEX = /^\s*rgba?\s*\(/ ; const RGBA_EXTRACT_REGEX = /^\s*rgba?\s*\(\s*([0-9]*)\s*,\s*([0-9]*)\s*,\s*([0-9]*)(?:\s*,\s*(.*)?)?\)\s*$/ ; const FLOAT_COLOR_CACHE = { } ; for ( const htmlColor in HTML_COLORS ) { FLOAT_COLOR_CACHE [ htmlColor ] = floatColor ( HTML_COLORS [ htmlColor ] ) ; } export function floatColor ( val ) { if ( typeof FLOAT_COLOR_CACHE [ val ] !== "undefined" ) return FLOAT_COLOR_CACHE [ val ] ; let r = 0 , g = 0 , b = 0 , a = 1 ; if ( val [ 0 ] === "#" ) { if ( val . length === 4 ) { r = parseInt ( val . charAt ( 1 ) + val . charAt ( 1 ) , 16 ) ; g = parseInt ( val . charAt ( 2 ) + val . charAt ( 2 ) , 16 ) ; b = parseInt ( val . charAt ( 3 ) + val . charAt ( 3 ) , 16 ) ; } else { r = parseInt ( val . charAt ( 1 ) + val . charAt ( 2 ) , 16 ) ; g = parseInt ( val . charAt ( 3 ) + val . charAt ( 4 ) , 16 ) ; b = parseInt ( val . charAt ( 5 ) + val . charAt ( 6 ) , 16 ) ; } } else if ( RGBA_TEST_REGEX . test ( val ) ) { const match = val . match ( RGBA_EXTRACT_REGEX ) ; if ( match ) { r = + match [ 1 ] ; g = + match [ 2 ] ; b = + match [ 3 ] ; if ( match [ 4 ] ) a = + match [ 4 ] ; } } a = ( a * 255 ) | 0 ; INT32 [ 0 ] = ( ( a << 24 ) | ( b << 16 ) | ( g << 8 ) | r ) & 0xfeffffff ; const color = FLOAT32 [ 0 ] ; FLOAT_COLOR_CACHE [ val ] = color ; return color ; } /**
 * In sigma, the graph is normalized into a [0, 1], [0, 1] square, before being given to the various renderers. This
 * helps dealing with quadtree in particular.
 * But at some point, we need to rescale it so that it takes the best place in the screen, ie. we always want to see two
 * nodes "touching" opposite sides of the graph, with the camera being at its default state.
 *
 * This function determines this ratio.
 */ export function getCorrectionRatio ( viewportDimensions , graphDimensions ) { const viewportRatio = viewportDimensions . height / viewportDimensions . width ; const graphRatio = graphDimensions . height / graphDimensions . width ; if ( ( viewportRatio < 1 && graphRatio > 1 ) || ( viewportRatio > 1 && graphRatio < 1 ) ) { return 1 ; } return Math . min ( Math . max ( graphRatio , 1 / graphRatio ) , Math . max ( 1 / viewportRatio , viewportRatio ) ) ; } /**
 * Function returning a matrix from the current state of the camera.
 */ export function matrixFromCamera ( state , viewportDimensions , graphDimensions , padding , inverse ? ) { const { angle , ratio , x , y } = state ; const { width , height } = viewportDimensions ; const matrix = identity ( ) ; const smallestDimension = Math . min ( width , height ) - 2 * padding ; const correctionRatio = getCorrectionRatio ( viewportDimensions , graphDimensions ) ; if ( ! inverse ) { multiply ( matrix , scale ( identity ( ) , 2 * ( smallestDimension / width ) * correctionRatio , 2 * ( smallestDimension / height ) * correctionRatio ) ) ; multiply ( matrix , rotate ( identity ( ) , - angle ) ) ; multiply ( matrix , scale ( identity ( ) , 1 / ratio ) ) ; multiply ( matrix , translate ( identity ( ) , - x , - y ) ) ; } else { multiply ( matrix , translate ( identity ( ) , x , y ) ) ; multiply ( matrix , scale ( identity ( ) , ratio ) ) ; multiply ( matrix , rotate ( identity ( ) , angle ) ) ; multiply ( matrix , scale ( identity ( ) , width / smallestDimension / 2 / correctionRatio , height / smallestDimension / 2 / correctionRatio ) ) ; } return matrix ; } /**
 * All these transformations we apply on the matrix to get it rescale the graph
 * as we want make it very hard to get pixel-perfect distances in WebGL. This
 * function returns a factor that properly cancels the matrix effect on lengths.
 *
 * [jacomyal]
 * To be fully honest, I can't really explain happens here... I notice that the
 * following ratio works (ie. it correctly compensates the matrix impact on all
 * camera states I could try):
 * > `R = size(V) / size(M * V) / W`
 * as long as `M * V` is in the direction of W (ie. parallel to (Ox)). It works
 * as well with H and a vector that transforms into something parallel to (Oy).
 *
 * Also, note that we use `angle` and not `-angle` (that would seem logical,
 * since we want to anticipate the rotation), because of the fact that in WebGL,
 * the image is vertically swapped.
 */ export function getMatrixImpact ( matrix , cameraState , viewportDimensions ) { const [ x , y ] = multiplyVec ( matrix , [ Math . cos ( cameraState . angle ) , Math . sin ( cameraState . angle ) , 0 ] ) ; return 1 / Math . sqrt ( Math . pow ( x , 2 ) + Math . pow ( y , 2 ) ) / viewportDimensions . width ; } /**
 * Function extracting the color at the given pixel.
 */ export function extractPixel ( gl , x , y , array ) { const data = array || new Uint8Array ( 4 ) ; gl . readPixels ( x , y , 1 , 1 , gl . RGBA , gl . UNSIGNED_BYTE , data ) ; return data ; } /**
 * Function used to know whether given webgl context can use 32 bits indices.
 */ export function canUse32BitsIndices ( gl ) { const webgl2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext ; return webgl2 || ! ! gl . getExtension ( "OES_element_index_uint" ) ; } /**
 * Check if the graph variable is a valid graph, and if sigma can render it.
 */ export function validateGraph ( graph ) { if ( ! isGraph ( graph ) ) throw new Error ( "Sigma: invalid graph instance." ) ; graph . forEachNode ( ( key , attributes ) => { if ( ! Number . isFinite ( attributes . x ) || ! Number . isFinite ( attributes . y ) ) { throw new Error ( `Sigma: Coordinates of node ${ key } are invalid. A node must have a numeric 'x' and 'y' attribute.` ) ; } } ) ; }