|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import * as util from '../../../util'; |
|
import * as is from '../../../is'; |
|
import { makeBoundingBox } from '../../../math'; |
|
import ElementTextureCache from './ele-texture-cache'; |
|
import LayeredTextureCache from './layered-texture-cache'; |
|
|
|
import arrowShapes from './arrow-shapes'; |
|
import drawingElements from './drawing-elements'; |
|
import drawingEdges from './drawing-edges'; |
|
import drawingImages from './drawing-images'; |
|
import drawingLabelText from './drawing-label-text'; |
|
import drawingNodes from './drawing-nodes'; |
|
import drawingRedraw from './drawing-redraw'; |
|
import drawingShapes from './drawing-shapes'; |
|
import exportImage from './export-image'; |
|
import nodeShapes from './node-shapes'; |
|
|
|
var CR = CanvasRenderer; |
|
var CRp = CanvasRenderer.prototype; |
|
|
|
CRp.CANVAS_LAYERS = 3; |
|
|
|
CRp.SELECT_BOX = 0; |
|
CRp.DRAG = 1; |
|
CRp.NODE = 2; |
|
|
|
CRp.BUFFER_COUNT = 3; |
|
|
|
CRp.TEXTURE_BUFFER = 0; |
|
CRp.MOTIONBLUR_BUFFER_NODE = 1; |
|
CRp.MOTIONBLUR_BUFFER_DRAG = 2; |
|
|
|
function CanvasRenderer( options ){ |
|
var r = this; |
|
|
|
r.data = { |
|
canvases: new Array( CRp.CANVAS_LAYERS ), |
|
contexts: new Array( CRp.CANVAS_LAYERS ), |
|
canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ), |
|
|
|
bufferCanvases: new Array( CRp.BUFFER_COUNT ), |
|
bufferContexts: new Array( CRp.CANVAS_LAYERS ), |
|
}; |
|
|
|
var tapHlOffAttr = '-webkit-tap-highlight-color'; |
|
var tapHlOffStyle = 'rgba(0,0,0,0)'; |
|
r.data.canvasContainer = document.createElement( 'div' ); |
|
var containerStyle = r.data.canvasContainer.style; |
|
r.data.canvasContainer.style[tapHlOffAttr] = tapHlOffStyle; |
|
containerStyle.position = 'relative'; |
|
containerStyle.zIndex = '0'; |
|
containerStyle.overflow = 'hidden'; |
|
|
|
var container = options.cy.container(); |
|
container.appendChild( r.data.canvasContainer ); |
|
container.style[tapHlOffAttr] = tapHlOffStyle; |
|
|
|
var styleMap = { |
|
'-webkit-user-select': 'none', |
|
'-moz-user-select': '-moz-none', |
|
'user-select': 'none', |
|
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)', |
|
'outline-style': 'none', |
|
}; |
|
|
|
if(is.ms()) { |
|
styleMap['-ms-touch-action'] = 'none'; |
|
styleMap['touch-action'] = 'none'; |
|
} |
|
|
|
for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){ |
|
var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' ); |
|
r.data.contexts[ i ] = canvas.getContext( '2d' ); |
|
Object.keys(styleMap).forEach((k) => { |
|
canvas.style[k] = styleMap[k]; |
|
}); |
|
canvas.style.position = 'absolute'; |
|
canvas.setAttribute( 'data-id', 'layer' + i ); |
|
canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i ); |
|
r.data.canvasContainer.appendChild( canvas ); |
|
|
|
r.data.canvasNeedsRedraw[ i ] = false; |
|
} |
|
r.data.topCanvas = r.data.canvases[0]; |
|
|
|
r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' ); |
|
r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' ); |
|
r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' ); |
|
|
|
for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){ |
|
r.data.bufferCanvases[ i ] = document.createElement( 'canvas' ); |
|
r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' ); |
|
r.data.bufferCanvases[ i ].style.position = 'absolute'; |
|
r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i ); |
|
r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 ); |
|
r.data.bufferCanvases[ i ].style.visibility = 'hidden'; |
|
|
|
} |
|
|
|
r.pathsEnabled = true; |
|
|
|
let emptyBb = makeBoundingBox(); |
|
|
|
let getBoxCenter = bb => ({ x: (bb.x1 + bb.x2)/2, y: (bb.y1 + bb.y2)/2 }); |
|
|
|
let getCenterOffset = bb => ({ x: -bb.w/2, y: -bb.h/2 }); |
|
|
|
let backgroundTimestampHasChanged = ele => { |
|
let _p = ele[0]._private; |
|
let same = _p.oldBackgroundTimestamp === _p.backgroundTimestamp; |
|
|
|
return !same; |
|
}; |
|
|
|
let getStyleKey = ele => ele[0]._private.nodeKey; |
|
let getLabelKey = ele => ele[0]._private.labelStyleKey; |
|
let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey; |
|
let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey; |
|
|
|
let drawElement = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElement( context, ele, bb, false, false, useEleOpacity ); |
|
let drawLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main', useEleOpacity ); |
|
let drawSourceLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source', useEleOpacity ); |
|
let drawTargetLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target', useEleOpacity ); |
|
|
|
let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.bodyBounds; }; |
|
let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; }; |
|
let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; }; |
|
let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; }; |
|
|
|
let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown; |
|
|
|
let getElementRotationPoint = ele => getBoxCenter( getElementBox(ele) ); |
|
|
|
let addTextMargin = (prefix, pt, ele) => { |
|
let pre = prefix ? prefix + '-' : ''; |
|
|
|
return { |
|
x: pt.x + ele.pstyle(pre + 'text-margin-x').pfValue, |
|
y: pt.y + ele.pstyle(pre + 'text-margin-y').pfValue |
|
}; |
|
}; |
|
|
|
let getRsPt = (ele, x, y) => { |
|
let rs = ele[0]._private.rscratch; |
|
|
|
return { x: rs[x], y: rs[y] }; |
|
}; |
|
|
|
let getLabelRotationPoint = ele => addTextMargin('', getRsPt(ele, 'labelX', 'labelY'), ele); |
|
let getSourceLabelRotationPoint = ele => addTextMargin('source', getRsPt(ele, 'sourceLabelX', 'sourceLabelY'), ele); |
|
let getTargetLabelRotationPoint = ele => addTextMargin('target', getRsPt(ele, 'targetLabelX', 'targetLabelY'), ele); |
|
|
|
let getElementRotationOffset = ele => getCenterOffset( getElementBox(ele) ); |
|
let getSourceLabelRotationOffset = ele => getCenterOffset( getSourceLabelBox(ele) ); |
|
let getTargetLabelRotationOffset = ele => getCenterOffset( getTargetLabelBox(ele) ); |
|
|
|
let getLabelRotationOffset = ele => { |
|
let bb = getLabelBox(ele); |
|
let p = getCenterOffset( getLabelBox(ele) ); |
|
|
|
if( ele.isNode() ){ |
|
switch( ele.pstyle('text-halign').value ){ |
|
case 'left': |
|
p.x = -bb.w; |
|
break; |
|
case 'right': |
|
p.x = 0; |
|
break; |
|
} |
|
|
|
switch( ele.pstyle('text-valign').value ){ |
|
case 'top': |
|
p.y = -bb.h; |
|
break; |
|
case 'bottom': |
|
p.y = 0; |
|
break; |
|
} |
|
} |
|
|
|
return p; |
|
}; |
|
|
|
let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, { |
|
getKey: getStyleKey, |
|
doesEleInvalidateKey: backgroundTimestampHasChanged, |
|
drawElement: drawElement, |
|
getBoundingBox: getElementBox, |
|
getRotationPoint: getElementRotationPoint, |
|
getRotationOffset: getElementRotationOffset, |
|
allowEdgeTxrCaching: false, |
|
allowParentTxrCaching: false |
|
} ); |
|
|
|
let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, { |
|
getKey: getLabelKey, |
|
drawElement: drawLabel, |
|
getBoundingBox: getLabelBox, |
|
getRotationPoint: getLabelRotationPoint, |
|
getRotationOffset: getLabelRotationOffset, |
|
isVisible: isLabelVisibleAtScale |
|
} ); |
|
|
|
let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, { |
|
getKey: getSourceLabelKey, |
|
drawElement: drawSourceLabel, |
|
getBoundingBox: getSourceLabelBox, |
|
getRotationPoint: getSourceLabelRotationPoint, |
|
getRotationOffset: getSourceLabelRotationOffset, |
|
isVisible: isLabelVisibleAtScale |
|
} ); |
|
|
|
let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, { |
|
getKey: getTargetLabelKey, |
|
drawElement: drawTargetLabel, |
|
getBoundingBox: getTargetLabelBox, |
|
getRotationPoint: getTargetLabelRotationPoint, |
|
getRotationOffset: getTargetLabelRotationOffset, |
|
isVisible: isLabelVisibleAtScale |
|
} ); |
|
|
|
let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r ); |
|
|
|
r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){ |
|
|
|
eleTxrCache.invalidateElements( eles ); |
|
lblTxrCache.invalidateElements( eles ); |
|
slbTxrCache.invalidateElements( eles ); |
|
tlbTxrCache.invalidateElements( eles ); |
|
|
|
|
|
lyrTxrCache.invalidateElements( eles ); |
|
|
|
|
|
for( let i = 0; i < eles.length; i++ ){ |
|
let _p = eles[i]._private; |
|
|
|
_p.oldBackgroundTimestamp = _p.backgroundTimestamp; |
|
} |
|
}); |
|
|
|
let refineInLayers = reqs => { |
|
for( var i = 0; i < reqs.length; i++ ){ |
|
lyrTxrCache.enqueueElementRefinement( reqs[i].ele ); |
|
} |
|
}; |
|
|
|
eleTxrCache.onDequeue(refineInLayers); |
|
lblTxrCache.onDequeue(refineInLayers); |
|
slbTxrCache.onDequeue(refineInLayers); |
|
tlbTxrCache.onDequeue(refineInLayers); |
|
} |
|
|
|
CRp.redrawHint = function( group, bool ){ |
|
var r = this; |
|
|
|
switch( group ){ |
|
case 'eles': |
|
r.data.canvasNeedsRedraw[ CRp.NODE ] = bool; |
|
break; |
|
case 'drag': |
|
r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool; |
|
break; |
|
case 'select': |
|
r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool; |
|
break; |
|
} |
|
}; |
|
|
|
|
|
var pathsImpld = typeof Path2D !== 'undefined'; |
|
|
|
CRp.path2dEnabled = function( on ){ |
|
if( on === undefined ){ |
|
return this.pathsEnabled; |
|
} |
|
|
|
this.pathsEnabled = on ? true : false; |
|
}; |
|
|
|
CRp.usePaths = function(){ |
|
return pathsImpld && this.pathsEnabled; |
|
}; |
|
|
|
CRp.setImgSmoothing = function( context, bool ){ |
|
if( context.imageSmoothingEnabled != null ){ |
|
context.imageSmoothingEnabled = bool; |
|
} else { |
|
context.webkitImageSmoothingEnabled = bool; |
|
context.mozImageSmoothingEnabled = bool; |
|
context.msImageSmoothingEnabled = bool; |
|
} |
|
}; |
|
|
|
CRp.getImgSmoothing = function( context ){ |
|
if( context.imageSmoothingEnabled != null ){ |
|
return context.imageSmoothingEnabled; |
|
} else { |
|
return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled; |
|
} |
|
}; |
|
|
|
CRp.makeOffscreenCanvas = function(width, height){ |
|
let canvas; |
|
|
|
if( typeof OffscreenCanvas !== typeof undefined ){ |
|
canvas = new OffscreenCanvas(width, height); |
|
} else { |
|
canvas = document.createElement('canvas'); |
|
canvas.width = width; |
|
canvas.height = height; |
|
} |
|
|
|
return canvas; |
|
}; |
|
|
|
[ |
|
arrowShapes, |
|
drawingElements, |
|
drawingEdges, |
|
drawingImages, |
|
drawingLabelText, |
|
drawingNodes, |
|
drawingRedraw, |
|
drawingShapes, |
|
exportImage, |
|
nodeShapes |
|
].forEach( function( props ){ |
|
util.extend( CRp, props ); |
|
} ); |
|
|
|
export default CR; |
|
|