|
|
|
|
|
import * as util from '../../../util'; |
|
import {drawPreparedRoundCorner} from "../../../round"; |
|
|
|
let CRp = {}; |
|
|
|
CRp.drawEdge = function( context, edge, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){ |
|
let r = this; |
|
let rs = edge._private.rscratch; |
|
|
|
if( shouldDrawOpacity && !edge.visible() ){ return; } |
|
|
|
|
|
if( rs.badLine || rs.allpts == null || isNaN(rs.allpts[0]) ){ |
|
return; |
|
} |
|
|
|
let bb; |
|
if( shiftToOriginWithBb ){ |
|
bb = shiftToOriginWithBb; |
|
|
|
context.translate( -bb.x1, -bb.y1 ); |
|
} |
|
|
|
let opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1; |
|
let lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1; |
|
|
|
let curveStyle = edge.pstyle('curve-style').value; |
|
let lineStyle = edge.pstyle('line-style').value; |
|
let edgeWidth = edge.pstyle('width').pfValue; |
|
let lineCap = edge.pstyle('line-cap').value; |
|
|
|
let effectiveLineOpacity = opacity * lineOpacity; |
|
|
|
let effectiveArrowOpacity = opacity * lineOpacity; |
|
|
|
let drawLine = ( strokeOpacity = effectiveLineOpacity) => { |
|
if (curveStyle === 'straight-triangle') { |
|
r.eleStrokeStyle( context, edge, strokeOpacity ); |
|
r.drawEdgeTrianglePath( |
|
edge, |
|
context, |
|
rs.allpts |
|
); |
|
} else { |
|
context.lineWidth = edgeWidth; |
|
context.lineCap = lineCap; |
|
|
|
r.eleStrokeStyle( context, edge, strokeOpacity ); |
|
r.drawEdgePath( |
|
edge, |
|
context, |
|
rs.allpts, |
|
lineStyle |
|
); |
|
|
|
context.lineCap = 'butt'; |
|
} |
|
}; |
|
|
|
let drawOverlay = () => { |
|
if( !shouldDrawOverlay ){ return; } |
|
|
|
r.drawEdgeOverlay( context, edge ); |
|
}; |
|
|
|
let drawUnderlay = () => { |
|
if( !shouldDrawOverlay ){ return; } |
|
|
|
r.drawEdgeUnderlay( context, edge ); |
|
}; |
|
|
|
let drawArrows = ( arrowOpacity = effectiveArrowOpacity) => { |
|
r.drawArrowheads( context, edge, arrowOpacity ); |
|
}; |
|
|
|
let drawText = () => { |
|
r.drawElementText( context, edge, null, drawLabel ); |
|
}; |
|
|
|
context.lineJoin = 'round'; |
|
|
|
let ghost = edge.pstyle('ghost').value === 'yes'; |
|
|
|
if( ghost ){ |
|
let gx = edge.pstyle('ghost-offset-x').pfValue; |
|
let gy = edge.pstyle('ghost-offset-y').pfValue; |
|
let ghostOpacity = edge.pstyle('ghost-opacity').value; |
|
let effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity; |
|
|
|
context.translate( gx, gy ); |
|
|
|
drawLine( effectiveGhostOpacity ); |
|
drawArrows( effectiveGhostOpacity ); |
|
|
|
context.translate( -gx, -gy ); |
|
} |
|
|
|
drawUnderlay(); |
|
drawLine(); |
|
drawArrows(); |
|
drawOverlay(); |
|
drawText(); |
|
|
|
if( shiftToOriginWithBb ){ |
|
context.translate( bb.x1, bb.y1 ); |
|
} |
|
}; |
|
|
|
const drawEdgeOverlayUnderlay = function( overlayOrUnderlay ) { |
|
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) { |
|
throw new Error('Invalid state'); |
|
} |
|
|
|
return function( context, edge ){ |
|
if( !edge.visible() ){ return; } |
|
|
|
let opacity = edge.pstyle(`${overlayOrUnderlay}-opacity`).value; |
|
|
|
if( opacity === 0 ){ return; } |
|
|
|
let r = this; |
|
let usePaths = r.usePaths(); |
|
let rs = edge._private.rscratch; |
|
|
|
let padding = edge.pstyle(`${overlayOrUnderlay}-padding`).pfValue; |
|
let width = 2 * padding; |
|
let color = edge.pstyle(`${overlayOrUnderlay}-color`).value; |
|
|
|
context.lineWidth = width; |
|
|
|
if( rs.edgeType === 'self' && !usePaths ){ |
|
context.lineCap = 'butt'; |
|
} else { |
|
context.lineCap = 'round'; |
|
} |
|
|
|
r.colorStrokeStyle( context, color[0], color[1], color[2], opacity ); |
|
|
|
r.drawEdgePath( |
|
edge, |
|
context, |
|
rs.allpts, |
|
'solid' |
|
); |
|
}; |
|
}; |
|
|
|
CRp.drawEdgeOverlay = drawEdgeOverlayUnderlay('overlay'); |
|
|
|
CRp.drawEdgeUnderlay = drawEdgeOverlayUnderlay('underlay'); |
|
|
|
|
|
CRp.drawEdgePath = function( edge, context, pts, type ){ |
|
let rs = edge._private.rscratch; |
|
let canvasCxt = context; |
|
let path; |
|
let pathCacheHit = false; |
|
let usePaths = this.usePaths(); |
|
let lineDashPattern = edge.pstyle('line-dash-pattern').pfValue; |
|
let lineDashOffset = edge.pstyle('line-dash-offset').pfValue; |
|
|
|
if( usePaths ){ |
|
let pathCacheKey = pts.join( '$' ); |
|
let keyMatches = rs.pathCacheKey && rs.pathCacheKey === pathCacheKey; |
|
|
|
if( keyMatches ){ |
|
path = context = rs.pathCache; |
|
pathCacheHit = true; |
|
} else { |
|
path = context = new Path2D(); |
|
rs.pathCacheKey = pathCacheKey; |
|
rs.pathCache = path; |
|
} |
|
} |
|
|
|
if( canvasCxt.setLineDash ){ |
|
switch( type ){ |
|
case 'dotted': |
|
canvasCxt.setLineDash( [ 1, 1 ] ); |
|
break; |
|
|
|
case 'dashed': |
|
canvasCxt.setLineDash( lineDashPattern ); |
|
canvasCxt.lineDashOffset = lineDashOffset; |
|
break; |
|
|
|
case 'solid': |
|
canvasCxt.setLineDash( [ ] ); |
|
break; |
|
} |
|
} |
|
|
|
if( !pathCacheHit && !rs.badLine ){ |
|
if( context.beginPath ){ context.beginPath(); } |
|
context.moveTo( pts[0], pts[1] ); |
|
|
|
switch( rs.edgeType ){ |
|
case 'bezier': |
|
case 'self': |
|
case 'compound': |
|
case 'multibezier': |
|
for( let i = 2; i + 3 < pts.length; i += 4 ){ |
|
context.quadraticCurveTo( pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] ); |
|
} |
|
break; |
|
|
|
case 'straight': |
|
case 'haystack': |
|
for( let i = 2; i + 1 < pts.length; i += 2 ) { |
|
context.lineTo( pts[ i ], pts[ i + 1] ); |
|
} |
|
break; |
|
case 'segments': |
|
if (rs.isRound) { |
|
for( let corner of rs.roundCorners ){ |
|
drawPreparedRoundCorner(context, corner); |
|
} |
|
context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] ); |
|
} else { |
|
for( let i = 2; i + 1 < pts.length; i += 2 ) { |
|
context.lineTo( pts[ i ], pts[ i + 1] ); |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
context = canvasCxt; |
|
if( usePaths ){ |
|
context.stroke( path ); |
|
} else { |
|
context.stroke(); |
|
} |
|
|
|
|
|
if( context.setLineDash ){ |
|
context.setLineDash( [ ] ); |
|
} |
|
|
|
}; |
|
|
|
|
|
CRp.drawEdgeTrianglePath = function( edge, context, pts ){ |
|
|
|
context.fillStyle = context.strokeStyle; |
|
|
|
let edgeWidth = edge.pstyle('width').pfValue; |
|
|
|
for( let i = 0; i + 1 < pts.length; i += 2 ){ |
|
const vector = [ pts[ i + 2 ] - pts[ i ], pts[ i + 3 ] - pts[ i + 1 ] ]; |
|
const length = Math.sqrt( vector[0] * vector[0] + vector[1] * vector[1] ); |
|
const normal = [ vector[1] / length, -vector[0] / length ]; |
|
const triangleHead = [ normal[0] * edgeWidth / 2, normal[1] * edgeWidth / 2 ]; |
|
|
|
context.beginPath(); |
|
context.moveTo( pts[ i ] - triangleHead[0], pts[ i + 1 ] - triangleHead[1] ); |
|
context.lineTo( pts[ i ] + triangleHead[0], pts[ i + 1 ] + triangleHead[1] ); |
|
context.lineTo( pts[ i + 2 ], pts[ i + 3 ] ); |
|
context.closePath(); |
|
context.fill(); |
|
} |
|
}; |
|
|
|
CRp.drawArrowheads = function( context, edge, opacity ){ |
|
let rs = edge._private.rscratch; |
|
let isHaystack = rs.edgeType === 'haystack'; |
|
|
|
if( !isHaystack ){ |
|
this.drawArrowhead( context, edge, 'source', rs.arrowStartX, rs.arrowStartY, rs.srcArrowAngle, opacity ); |
|
} |
|
|
|
this.drawArrowhead( context, edge, 'mid-target', rs.midX, rs.midY, rs.midtgtArrowAngle, opacity ); |
|
|
|
this.drawArrowhead( context, edge, 'mid-source', rs.midX, rs.midY, rs.midsrcArrowAngle, opacity ); |
|
|
|
if( !isHaystack ){ |
|
this.drawArrowhead( context, edge, 'target', rs.arrowEndX, rs.arrowEndY, rs.tgtArrowAngle, opacity ); |
|
} |
|
}; |
|
|
|
CRp.drawArrowhead = function( context, edge, prefix, x, y, angle, opacity ){ |
|
if( isNaN( x ) || x == null || isNaN( y ) || y == null || isNaN( angle ) || angle == null ){ return; } |
|
|
|
let self = this; |
|
let arrowShape = edge.pstyle( prefix + '-arrow-shape' ).value; |
|
if( arrowShape === 'none' ) { return; } |
|
|
|
let arrowClearFill = edge.pstyle( prefix + '-arrow-fill' ).value === 'hollow' ? 'both' : 'filled'; |
|
let arrowFill = edge.pstyle( prefix + '-arrow-fill' ).value; |
|
let edgeWidth = edge.pstyle( 'width' ).pfValue; |
|
|
|
let pArrowWidth = edge.pstyle( prefix + '-arrow-width' ); |
|
let arrowWidth = pArrowWidth.value === 'match-line' ? edgeWidth : pArrowWidth.pfValue; |
|
if (pArrowWidth.units === '%') arrowWidth *= edgeWidth; |
|
|
|
let edgeOpacity = edge.pstyle( 'opacity' ).value; |
|
|
|
if( opacity === undefined ){ |
|
opacity = edgeOpacity; |
|
} |
|
|
|
let gco = context.globalCompositeOperation; |
|
|
|
if( opacity !== 1 || arrowFill === 'hollow' ){ |
|
context.globalCompositeOperation = 'destination-out'; |
|
|
|
self.colorFillStyle( context, 255, 255, 255, 1 ); |
|
self.colorStrokeStyle( context, 255, 255, 255, 1 ); |
|
|
|
self.drawArrowShape( edge, context, |
|
arrowClearFill, edgeWidth, arrowShape, arrowWidth, x, y, angle |
|
); |
|
|
|
context.globalCompositeOperation = gco; |
|
} |
|
|
|
let color = edge.pstyle( prefix + '-arrow-color' ).value; |
|
self.colorFillStyle( context, color[0], color[1], color[2], opacity ); |
|
self.colorStrokeStyle( context, color[0], color[1], color[2], opacity ); |
|
|
|
self.drawArrowShape( edge, context, |
|
arrowFill, edgeWidth, arrowShape, arrowWidth, x, y, angle |
|
); |
|
}; |
|
|
|
CRp.drawArrowShape = function( edge, context, fill, edgeWidth, shape, shapeWidth, x, y, angle ){ |
|
let r = this; |
|
let usePaths = this.usePaths() && shape !== 'triangle-cross'; |
|
let pathCacheHit = false; |
|
let path; |
|
let canvasContext = context; |
|
let translation = { x, y }; |
|
let scale = edge.pstyle( 'arrow-scale' ).value; |
|
let size = this.getArrowWidth( edgeWidth, scale ); |
|
let shapeImpl = r.arrowShapes[ shape ]; |
|
|
|
if( usePaths ){ |
|
let cache = r.arrowPathCache = r.arrowPathCache || []; |
|
let key = util.hashString(shape); |
|
let cachedPath = cache[ key ]; |
|
|
|
if( cachedPath != null ){ |
|
path = context = cachedPath; |
|
pathCacheHit = true; |
|
} else { |
|
path = context = new Path2D(); |
|
cache[ key ] = path; |
|
} |
|
} |
|
|
|
if( !pathCacheHit ){ |
|
if( context.beginPath ){ context.beginPath(); } |
|
if( usePaths ){ |
|
shapeImpl.draw( context, 1, 0, { x: 0, y: 0 }, 1 ); |
|
} else { |
|
shapeImpl.draw( context, size, angle, translation, edgeWidth ); |
|
} |
|
if( context.closePath ){ context.closePath(); } |
|
} |
|
|
|
context = canvasContext; |
|
|
|
if( usePaths ){ |
|
context.translate( x, y ); |
|
context.rotate( angle ); |
|
context.scale( size, size ); |
|
} |
|
|
|
if( fill === 'filled' || fill === 'both' ){ |
|
if( usePaths ){ |
|
context.fill( path ); |
|
} else { |
|
context.fill(); |
|
} |
|
} |
|
|
|
if( fill === 'hollow' || fill === 'both' ){ |
|
context.lineWidth = shapeWidth / (usePaths ? size : 1); |
|
context.lineJoin = 'miter'; |
|
|
|
if( usePaths ){ |
|
context.stroke( path ); |
|
} else { |
|
context.stroke(); |
|
} |
|
} |
|
|
|
if( usePaths ){ |
|
context.scale( 1/size, 1/size ); |
|
context.rotate( -angle ); |
|
context.translate( -x, -y ); |
|
} |
|
}; |
|
|
|
export default CRp; |
|
|