|
|
|
|
|
import * as is from '../../../is'; |
|
import {expandPolygon, joinLines} from '../../../math'; |
|
import * as util from '../../../util'; |
|
import * as round from "../../../round"; |
|
import * as math from "../../../math"; |
|
|
|
let CRp = {}; |
|
|
|
CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){ |
|
let r = this; |
|
let nodeWidth, nodeHeight; |
|
let _p = node._private; |
|
let rs = _p.rscratch; |
|
let pos = node.position(); |
|
|
|
if( !is.number( pos.x ) || !is.number( pos.y ) ){ |
|
return; |
|
} |
|
|
|
if( shouldDrawOpacity && !node.visible() ){ return; } |
|
|
|
let eleOpacity = shouldDrawOpacity ? node.effectiveOpacity() : 1; |
|
|
|
let usePaths = r.usePaths(); |
|
let path; |
|
let pathCacheHit = false; |
|
|
|
let padding = node.padding(); |
|
|
|
nodeWidth = node.width() + 2 * padding; |
|
nodeHeight = node.height() + 2 * padding; |
|
|
|
|
|
|
|
|
|
let bb; |
|
if( shiftToOriginWithBb ){ |
|
bb = shiftToOriginWithBb; |
|
|
|
context.translate( -bb.x1, -bb.y1 ); |
|
} |
|
|
|
|
|
|
|
|
|
let bgImgProp = node.pstyle( 'background-image' ); |
|
let urls = bgImgProp.value; |
|
let urlDefined = new Array( urls.length ); |
|
let image = new Array( urls.length ); |
|
let numImages = 0; |
|
for( let i = 0; i < urls.length; i++ ){ |
|
let url = urls[i]; |
|
let defd = urlDefined[i] = url != null && url !== 'none'; |
|
|
|
if( defd ){ |
|
let bgImgCrossOrigin = node.cy().style().getIndexedStyle(node, 'background-image-crossorigin', 'value', i); |
|
|
|
numImages++; |
|
|
|
|
|
image[i] = r.getCachedImage( url, bgImgCrossOrigin, function(){ |
|
_p.backgroundTimestamp = Date.now(); |
|
|
|
node.emitAndNotify('background'); |
|
} ); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
let darkness = node.pstyle('background-blacken').value; |
|
let borderWidth = node.pstyle('border-width').pfValue; |
|
let bgOpacity = node.pstyle('background-opacity').value * eleOpacity; |
|
let borderColor = node.pstyle('border-color').value; |
|
let borderStyle = node.pstyle('border-style').value; |
|
let borderJoin = node.pstyle('border-join').value; |
|
let borderCap = node.pstyle('border-cap').value; |
|
let borderPosition = node.pstyle('border-position').value; |
|
let borderPattern = node.pstyle('border-dash-pattern').pfValue; |
|
let borderOffset = node.pstyle('border-dash-offset').pfValue; |
|
let borderOpacity = node.pstyle('border-opacity').value * eleOpacity; |
|
let outlineWidth = node.pstyle('outline-width').pfValue; |
|
let outlineColor = node.pstyle('outline-color').value; |
|
let outlineStyle = node.pstyle('outline-style').value; |
|
let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity; |
|
let outlineOffset = node.pstyle('outline-offset').value; |
|
let cornerRadius = node.pstyle('corner-radius').value; |
|
if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue; |
|
|
|
let setupShapeColor = ( bgOpy = bgOpacity ) => { |
|
r.eleFillStyle( context, node, bgOpy ); |
|
}; |
|
|
|
let setupBorderColor = ( bdrOpy = borderOpacity ) => { |
|
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy ); |
|
}; |
|
|
|
let setupOutlineColor = ( otlnOpy = outlineOpacity ) => { |
|
r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy ); |
|
}; |
|
|
|
|
|
|
|
|
|
let getPath = (width, height, shape, points) => { |
|
let pathCache = r.nodePathCache = r.nodePathCache || []; |
|
|
|
let key = util.hashStrings( |
|
shape === 'polygon' ? shape + ',' + points.join(',') : shape, |
|
'' + height, |
|
'' + width, |
|
'' + cornerRadius |
|
); |
|
|
|
let cachedPath = pathCache[ key ]; |
|
let path; |
|
let cacheHit = false; |
|
|
|
if( cachedPath != null ){ |
|
path = cachedPath; |
|
cacheHit = true; |
|
rs.pathCache = path; |
|
} else { |
|
path = new Path2D(); |
|
pathCache[ key ] = rs.pathCache = path; |
|
} |
|
|
|
return { |
|
path, |
|
cacheHit |
|
}; |
|
}; |
|
|
|
let styleShape = node.pstyle('shape').strValue; |
|
let shapePts = node.pstyle('shape-polygon-points').pfValue; |
|
|
|
if( usePaths ){ |
|
context.translate( pos.x, pos.y ); |
|
|
|
const shapePath = getPath(nodeWidth, nodeHeight, styleShape, shapePts); |
|
path = shapePath.path; |
|
pathCacheHit = shapePath.cacheHit; |
|
} |
|
|
|
let drawShape = () => { |
|
if( !pathCacheHit ){ |
|
|
|
let npos = pos; |
|
|
|
if( usePaths ){ |
|
npos = { |
|
x: 0, |
|
y: 0 |
|
}; |
|
} |
|
|
|
r.nodeShapes[ r.getNodeShape( node ) ].draw( |
|
( path || context ), |
|
npos.x, |
|
npos.y, |
|
nodeWidth, |
|
nodeHeight, cornerRadius, rs ); |
|
} |
|
|
|
if( usePaths ){ |
|
context.fill( path ); |
|
} else { |
|
context.fill(); |
|
} |
|
}; |
|
|
|
let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => { |
|
let prevBging = _p.backgrounding; |
|
let totalCompleted = 0; |
|
|
|
for( let i = 0; i < image.length; i++ ){ |
|
const bgContainment = node.cy().style().getIndexedStyle(node, 'background-image-containment', 'value', i); |
|
if( inside && bgContainment === 'over' || !inside && bgContainment === 'inside' ){ |
|
totalCompleted++; |
|
continue; |
|
} |
|
|
|
if( urlDefined[i] && image[i].complete && !image[i].error ){ |
|
totalCompleted++; |
|
r.drawInscribedImage( context, image[i], node, i, nodeOpacity ); |
|
} |
|
} |
|
|
|
_p.backgrounding = !(totalCompleted === numImages); |
|
if( prevBging !== _p.backgrounding ){ |
|
node.updateStyle( false ); |
|
} |
|
}; |
|
|
|
let drawPie = ( redrawShape = false, pieOpacity = eleOpacity ) => { |
|
if( r.hasPie( node ) ){ |
|
r.drawPie( context, node, pieOpacity ); |
|
|
|
|
|
if( redrawShape ){ |
|
|
|
if( !usePaths ){ |
|
r.nodeShapes[ r.getNodeShape( node ) ].draw( |
|
context, |
|
pos.x, |
|
pos.y, |
|
nodeWidth, |
|
nodeHeight, cornerRadius, rs ); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
let darken = ( darkenOpacity = eleOpacity ) => { |
|
let opacity = ( darkness > 0 ? darkness : -darkness ) * darkenOpacity; |
|
let c = darkness > 0 ? 0 : 255; |
|
|
|
if( darkness !== 0 ){ |
|
r.colorFillStyle( context, c, c, c, opacity ); |
|
|
|
if( usePaths ){ |
|
context.fill( path ); |
|
} else { |
|
context.fill(); |
|
} |
|
} |
|
}; |
|
|
|
let drawBorder = () => { |
|
if( borderWidth > 0 ){ |
|
|
|
context.lineWidth = borderWidth; |
|
context.lineCap = borderCap; |
|
context.lineJoin = borderJoin; |
|
|
|
if( context.setLineDash ){ |
|
switch( borderStyle ){ |
|
case 'dotted': |
|
context.setLineDash( [ 1, 1 ] ); |
|
break; |
|
|
|
case 'dashed': |
|
context.setLineDash( borderPattern ); |
|
context.lineDashOffset = borderOffset; |
|
break; |
|
|
|
case 'solid': |
|
case 'double': |
|
context.setLineDash( [ ] ); |
|
break; |
|
} |
|
} |
|
|
|
if ( borderPosition !== 'center') { |
|
context.save(); |
|
context.lineWidth *= 2; |
|
if (borderPosition === 'inside') { |
|
usePaths ? context.clip(path) : context.clip(); |
|
} else { |
|
const region = new Path2D(); |
|
region.rect( |
|
-nodeWidth / 2 - borderWidth, |
|
-nodeHeight / 2 - borderWidth, |
|
nodeWidth + 2 * borderWidth, |
|
nodeHeight + 2 * borderWidth |
|
); |
|
region.addPath(path); |
|
context.clip(region, 'evenodd'); |
|
} |
|
usePaths ? context.stroke(path) : context.stroke(); |
|
context.restore(); |
|
} else { |
|
usePaths ? context.stroke(path) : context.stroke(); |
|
} |
|
|
|
if( borderStyle === 'double' ){ |
|
context.lineWidth = borderWidth / 3; |
|
|
|
let gco = context.globalCompositeOperation; |
|
context.globalCompositeOperation = 'destination-out'; |
|
|
|
if( usePaths ){ |
|
context.stroke( path ); |
|
} else { |
|
context.stroke(); |
|
} |
|
|
|
context.globalCompositeOperation = gco; |
|
} |
|
|
|
|
|
if( context.setLineDash ){ |
|
context.setLineDash( [ ] ); |
|
} |
|
} |
|
}; |
|
|
|
let drawOutline = () => { |
|
if( outlineWidth > 0 ){ |
|
context.lineWidth = outlineWidth; |
|
context.lineCap = 'butt'; |
|
|
|
if( context.setLineDash ){ |
|
switch( outlineStyle ){ |
|
case 'dotted': |
|
context.setLineDash( [ 1, 1 ] ); |
|
break; |
|
|
|
case 'dashed': |
|
context.setLineDash( [ 4, 2 ] ); |
|
break; |
|
|
|
case 'solid': |
|
case 'double': |
|
context.setLineDash( [ ] ); |
|
break; |
|
} |
|
} |
|
|
|
let npos = pos; |
|
|
|
if( usePaths ){ |
|
npos = { |
|
x: 0, |
|
y: 0 |
|
}; |
|
} |
|
|
|
let shape = r.getNodeShape( node ); |
|
|
|
let bWidth = borderWidth; |
|
if( borderPosition === 'inside' ) bWidth = 0; |
|
if( borderPosition === 'outside' ) bWidth *= 2; |
|
|
|
let scaleX = (nodeWidth + bWidth + (outlineWidth + outlineOffset)) / nodeWidth; |
|
let scaleY = (nodeHeight + bWidth + (outlineWidth + outlineOffset)) / nodeHeight; |
|
let sWidth = nodeWidth * scaleX; |
|
let sHeight = nodeHeight * scaleY; |
|
|
|
let points = r.nodeShapes[ shape ].points; |
|
let path; |
|
|
|
if (usePaths) { |
|
let outlinePath = getPath(sWidth, sHeight, shape, points); |
|
path = outlinePath.path; |
|
} |
|
|
|
|
|
|
|
if (shape === "ellipse") { |
|
r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight); |
|
} else if ([ |
|
'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon', |
|
'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag' |
|
].includes(shape)) { |
|
let sMult = 0; |
|
let offsetX = 0; |
|
let offsetY = 0; |
|
|
|
if (shape === 'round-diamond') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * 1.4; |
|
} else if (shape === 'round-heptagon') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * 1.075; |
|
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 35; |
|
} else if (shape === 'round-hexagon') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12; |
|
} else if (shape === 'round-pentagon') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * 1.13; |
|
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 15; |
|
} else if (shape === 'round-tag') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12; |
|
offsetX = (bWidth/2 + outlineWidth + outlineOffset) * .07; |
|
} else if (shape === 'round-triangle') { |
|
sMult = (bWidth + outlineOffset + outlineWidth) * (Math.PI/2); |
|
offsetY = -(bWidth + outlineOffset/2 + outlineWidth) / Math.PI; |
|
} |
|
|
|
if (sMult !== 0) { |
|
scaleX = (nodeWidth + sMult)/nodeWidth; |
|
sWidth = nodeWidth * scaleX; |
|
if ( ! ['round-hexagon', 'round-tag'].includes(shape) ) { |
|
scaleY = (nodeHeight + sMult)/nodeHeight; |
|
sHeight = nodeHeight * scaleY; |
|
} |
|
} |
|
|
|
cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius; |
|
|
|
const halfW = sWidth / 2; |
|
const halfH = sHeight / 2; |
|
const radius = cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2; |
|
const p = new Array( points.length / 2 ); |
|
const corners = new Array( points.length / 2 ); |
|
|
|
for ( let i = 0; i < points.length / 2; i++ ){ |
|
p[i] = { |
|
x: npos.x + offsetX + halfW * points[ i * 2 ], |
|
y: npos.y + offsetY + halfH * points[ i * 2 + 1 ] |
|
}; |
|
} |
|
|
|
let i, p1, p2, p3, len = p.length; |
|
|
|
p1 = p[ len - 1 ]; |
|
|
|
for( i = 0; i < len; i++ ){ |
|
p2 = p[ (i) % len ]; |
|
p3 = p[ (i + 1) % len ]; |
|
corners[ i ] = round.getRoundCorner( p1, p2, p3, radius ); |
|
p1 = p2; |
|
p2 = p3; |
|
} |
|
|
|
r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners ); |
|
} else if (['roundrectangle', 'round-rectangle'].includes(shape)) { |
|
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; |
|
r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); |
|
} else if (['cutrectangle', 'cut-rectangle'].includes(shape)) { |
|
cornerRadius = cornerRadius === 'auto' ? math.getCutRectangleCornerLength( sWidth, sHeight ) : cornerRadius; |
|
r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, null ,cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 4 ); |
|
} else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) { |
|
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius; |
|
r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 ); |
|
} else if (shape === "barrel") { |
|
r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight); |
|
} else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) { |
|
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth; |
|
points = joinLines(expandPolygon(points, pad)); |
|
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); |
|
} else { |
|
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth; |
|
points = joinLines(expandPolygon(points, -pad)); |
|
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points); |
|
} |
|
|
|
if( usePaths ){ |
|
context.stroke( path ); |
|
} else { |
|
context.stroke(); |
|
} |
|
|
|
if( outlineStyle === 'double' ){ |
|
context.lineWidth = bWidth / 3; |
|
|
|
let gco = context.globalCompositeOperation; |
|
context.globalCompositeOperation = 'destination-out'; |
|
|
|
if( usePaths ){ |
|
context.stroke( path ); |
|
} else { |
|
context.stroke(); |
|
} |
|
|
|
context.globalCompositeOperation = gco; |
|
} |
|
|
|
|
|
if( context.setLineDash ){ |
|
context.setLineDash( [ ] ); |
|
} |
|
} |
|
}; |
|
|
|
let drawOverlay = () => { |
|
if( shouldDrawOverlay ){ |
|
r.drawNodeOverlay( context, node, pos, nodeWidth, nodeHeight ); |
|
} |
|
}; |
|
|
|
let drawUnderlay = () => { |
|
if( shouldDrawOverlay ){ |
|
r.drawNodeUnderlay( context, node, pos, nodeWidth, nodeHeight ); |
|
} |
|
}; |
|
|
|
let drawText = () => { |
|
r.drawElementText( context, node, null, drawLabel ); |
|
}; |
|
|
|
let ghost = node.pstyle('ghost').value === 'yes'; |
|
|
|
if( ghost ){ |
|
let gx = node.pstyle('ghost-offset-x').pfValue; |
|
let gy = node.pstyle('ghost-offset-y').pfValue; |
|
let ghostOpacity = node.pstyle('ghost-opacity').value; |
|
let effGhostOpacity = ghostOpacity * eleOpacity; |
|
|
|
context.translate( gx, gy ); |
|
|
|
setupOutlineColor(); |
|
drawOutline(); |
|
setupShapeColor( ghostOpacity * bgOpacity ); |
|
drawShape(); |
|
drawImages( effGhostOpacity, true ); |
|
setupBorderColor( ghostOpacity * borderOpacity ); |
|
drawBorder(); |
|
drawPie( darkness !== 0 || borderWidth !== 0 ); |
|
drawImages( effGhostOpacity, false ); |
|
darken( effGhostOpacity ); |
|
|
|
context.translate( -gx, -gy ); |
|
} |
|
|
|
if( usePaths ){ |
|
context.translate( -pos.x, -pos.y ); |
|
} |
|
drawUnderlay(); |
|
if( usePaths ){ |
|
context.translate( pos.x, pos.y ); |
|
} |
|
|
|
setupOutlineColor(); |
|
drawOutline(); |
|
setupShapeColor(); |
|
drawShape(); |
|
drawImages(eleOpacity, true); |
|
setupBorderColor(); |
|
drawBorder(); |
|
drawPie( darkness !== 0 || borderWidth !== 0 ); |
|
drawImages(eleOpacity, false); |
|
|
|
darken(); |
|
|
|
if( usePaths ){ |
|
context.translate( -pos.x, -pos.y ); |
|
} |
|
|
|
drawText(); |
|
|
|
drawOverlay(); |
|
|
|
|
|
|
|
|
|
if( shiftToOriginWithBb ){ |
|
context.translate( bb.x1, bb.y1 ); |
|
} |
|
|
|
}; |
|
|
|
const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) { |
|
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) { |
|
throw new Error('Invalid state'); |
|
} |
|
|
|
return function( context, node, pos, nodeWidth, nodeHeight ){ |
|
let r = this; |
|
|
|
if( !node.visible() ){ return; } |
|
|
|
let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue; |
|
let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value; |
|
let color = node.pstyle( `${overlayOrUnderlay}-color` ).value; |
|
let shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value; |
|
let radius = node.pstyle( `${overlayOrUnderlay}-corner-radius` ).value; |
|
|
|
if( opacity > 0 ){ |
|
pos = pos || node.position(); |
|
|
|
if( nodeWidth == null || nodeHeight == null ){ |
|
let padding = node.padding(); |
|
|
|
nodeWidth = node.width() + 2 * padding; |
|
nodeHeight = node.height() + 2 * padding; |
|
} |
|
|
|
r.colorFillStyle( context, color[0], color[1], color[2], opacity ); |
|
|
|
r.nodeShapes[shape].draw( |
|
context, |
|
pos.x, |
|
pos.y, |
|
nodeWidth + padding * 2, |
|
nodeHeight + padding * 2, |
|
radius |
|
); |
|
|
|
context.fill(); |
|
} |
|
}; |
|
}; |
|
|
|
CRp.drawNodeOverlay = drawNodeOverlayUnderlay('overlay'); |
|
|
|
CRp.drawNodeUnderlay = drawNodeOverlayUnderlay('underlay'); |
|
|
|
|
|
CRp.hasPie = function( node ){ |
|
node = node[0]; |
|
|
|
return node._private.hasPie; |
|
}; |
|
|
|
CRp.drawPie = function( context, node, nodeOpacity, pos ){ |
|
node = node[0]; |
|
pos = pos || node.position(); |
|
|
|
let cyStyle = node.cy().style(); |
|
let pieSize = node.pstyle( 'pie-size' ); |
|
let x = pos.x; |
|
let y = pos.y; |
|
let nodeW = node.width(); |
|
let nodeH = node.height(); |
|
let radius = Math.min( nodeW, nodeH ) / 2; |
|
let lastPercent = 0; |
|
let usePaths = this.usePaths(); |
|
|
|
if( usePaths ){ |
|
x = 0; |
|
y = 0; |
|
} |
|
|
|
if( pieSize.units === '%' ){ |
|
radius = radius * pieSize.pfValue; |
|
} else if( pieSize.pfValue !== undefined ){ |
|
radius = pieSize.pfValue / 2; |
|
} |
|
|
|
for( let i = 1; i <= cyStyle.pieBackgroundN; i++ ){ |
|
let size = node.pstyle( 'pie-' + i + '-background-size' ).value; |
|
let color = node.pstyle( 'pie-' + i + '-background-color' ).value; |
|
let opacity = node.pstyle( 'pie-' + i + '-background-opacity' ).value * nodeOpacity; |
|
let percent = size / 100; |
|
|
|
|
|
if( percent + lastPercent > 1 ){ |
|
percent = 1 - lastPercent; |
|
} |
|
|
|
let angleStart = 1.5 * Math.PI + 2 * Math.PI * lastPercent; |
|
let angleDelta = 2 * Math.PI * percent; |
|
let angleEnd = angleStart + angleDelta; |
|
|
|
|
|
|
|
|
|
|
|
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){ |
|
continue; |
|
} |
|
|
|
context.beginPath(); |
|
context.moveTo( x, y ); |
|
context.arc( x, y, radius, angleStart, angleEnd ); |
|
context.closePath(); |
|
|
|
this.colorFillStyle( context, color[0], color[1], color[2], opacity ); |
|
|
|
context.fill(); |
|
|
|
lastPercent += percent; |
|
} |
|
|
|
}; |
|
|
|
|
|
export default CRp; |
|
|