/* The canvas renderer was written by Yue Dong. Modifications tracked on Github. */ /* global OffscreenCanvas */ 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; var containerWindow = r.cy.window(); var document = containerWindow.document; 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' ); // eslint-disable-line no-undef 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' ); // eslint-disable-line no-undef 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' ); // eslint-disable-line no-undef 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.data.canvasContainer.appendChild(r.data.bufferCanvases[i]); } 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 - (bb.leftPad || 0); break; case 'right': p.x = -(bb.rightPad || 0); break; } switch( ele.pstyle('text-valign').value ){ case 'top': p.y = -bb.h - (bb.topPad || 0); break; case 'bottom': p.y = -(bb.botPad || 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 ){ // each cache should check for sub-key diff to see that the update affects that cache particularly eleTxrCache.invalidateElements( eles ); lblTxrCache.invalidateElements( eles ); slbTxrCache.invalidateElements( eles ); tlbTxrCache.invalidateElements( eles ); // any change invalidates the layers lyrTxrCache.invalidateElements( eles ); // update the old bg timestamp so diffs can be done in the ele txr caches 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; } }; // whether to use Path2D caching for drawing 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 { var containerWindow = this.cy.window(); var document = containerWindow.document; canvas = document.createElement('canvas'); // eslint-disable-line no-undef 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;