Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node outlines #3158

Merged
merged 12 commits into from
Nov 10, 2023
8 changes: 8 additions & 0 deletions documentation/md/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ Border:
* **`border-color`** : The colour of the node's border.
* **`border-opacity`** : The opacity of the node's border.

Outline:

* **`outline-width`** : The size of the node's outline.
* **`outline-style`** : The style of the node's outline; may be `solid`, `dotted`, `dashed`, or `double`.
* **`outline-color`** : The colour of the node's outline.
* **`outline-opacity`** : The opacity of the node's outline.
* **`outline-offset`** : The offset of the node's outline.

Padding:

A padding defines an addition to a node's dimension. For example, `padding` adds to a node's outer (i.e. total) width and height. This can be used to add spacing between a compound node parent and its children.
Expand Down
53 changes: 52 additions & 1 deletion src/collection/dimensions/bounds.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as is from '../../is';
import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox } from '../../math';
import { assignBoundingBox, expandBoundingBoxSides, clearBoundingBox, expandBoundingBox, makeBoundingBox, copyBoundingBox, shiftBoundingBox, updateBoundingBox } from '../../math';
import { defaults, getPrefixedProperty, hashIntsArray } from '../../util';

let fn, elesfn;
Expand Down Expand Up @@ -428,6 +428,52 @@ let updateBoundsFromLabel = function( bounds, ele, prefix ){
return bounds;
};

let updateBoundsFromOutline = function( bounds, ele ){
if( ele.cy().headless() ){ return; }

let outlineOpacity = ele.pstyle('outline-opacity').value;
let outlineWidth = ele.pstyle('outline-width').value;

if (outlineOpacity > 0 && outlineWidth > 0) {
let outlineOffset = ele.pstyle('outline-offset').value;
let nodeShape = ele.pstyle( 'shape' ).value;

let outlineSize = outlineWidth + outlineOffset;
let scaleX = (bounds.w + outlineSize * 2) / bounds.w;
let scaleY = (bounds.h + outlineSize * 2) / bounds.h;
let xOffset = 0;
let yOffset = 0;

if (["diamond", "pentagon", "round-triangle"].includes(nodeShape)) {
scaleX = (bounds.w + outlineSize * 2.4) / bounds.w;
yOffset = -outlineSize/3.6;
} else if (["concave-hexagon", "rhomboid", "right-rhomboid"].includes(nodeShape)) {
scaleX = (bounds.w + outlineSize * 2.4) / bounds.w;
} else if (nodeShape === "star") {
scaleX = (bounds.w + outlineSize * 2.8) / bounds.w;
scaleY = (bounds.h + outlineSize * 2.6) / bounds.h;
yOffset = -outlineSize / 3.8;
} else if (nodeShape === "triangle") {
scaleX = (bounds.w + outlineSize * 2.8) / bounds.w;
scaleY = (bounds.h + outlineSize * 2.4) / bounds.h;
yOffset = -outlineSize/1.4;
} else if (nodeShape === "vee") {
scaleX = (bounds.w + outlineSize * 4.4) / bounds.w;
scaleY = (bounds.h + outlineSize * 3.8) / bounds.h;
yOffset = -outlineSize * .5;
}

let hDelta = (bounds.h * scaleY) - bounds.h;
let wDelta = (bounds.w * scaleX) - bounds.w;
expandBoundingBoxSides(bounds, [Math.ceil(hDelta/2), Math.ceil(wDelta/2)]);

if (xOffset != 0 || yOffset !== 0) {
let oBounds = shiftBoundingBox(bounds, xOffset, yOffset);
updateBoundingBox(bounds, oBounds);
}
}
};

// get the bounding box of the elements (in raw model position)
let boundingBoxImpl = function( ele, options ){
let cy = ele._private.cy;
Expand Down Expand Up @@ -510,6 +556,9 @@ let boundingBoxImpl = function( ele, options ){

updateBounds( bounds, ex1, ey1, ex2, ey2 );

if( styleEnabled && options.includeOutlines ){
updateBoundsFromOutline( bounds, ele );
}
} else if( isEdge && options.includeEdges ){

if( styleEnabled && !headless ){
Expand Down Expand Up @@ -735,6 +784,7 @@ let getKey = function( opts ){
key += tf( opts.includeSourceLabels );
key += tf( opts.includeTargetLabels );
key += tf( opts.includeOverlays );
key += tf( opts.includeOutlines );

return key;
};
Expand Down Expand Up @@ -824,6 +874,7 @@ let defBbOpts = {
includeTargetLabels: true,
includeOverlays: true,
includeUnderlays: true,
includeOutlines: true,
useCache: true
};

Expand Down
184 changes: 169 additions & 15 deletions src/extensions/renderer/canvas/drawing-nodes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global Path2D */

import * as is from '../../../is';
import { expandPolygon, joinLines } from '../../../math';
import * as util from '../../../util';

let CRp = {};
Expand Down Expand Up @@ -74,6 +75,11 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
let borderColor = node.pstyle('border-color').value;
let borderStyle = node.pstyle('border-style').value;
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;

context.lineJoin = 'miter'; // so borders are square with the node shape

Expand All @@ -85,33 +91,50 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy );
};

let setupOutlineColor = ( otlnOpy = outlineOpacity ) => {
r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy );
};

//
// setup shape

let styleShape = node.pstyle('shape').strValue;
let shapePts = node.pstyle('shape-polygon-points').pfValue;

if( usePaths ){
context.translate( pos.x, pos.y );

let getPath = (width, height, shape, points) => {
let pathCache = r.nodePathCache = r.nodePathCache || [];

let key = util.hashStrings(
styleShape === 'polygon' ? styleShape + ',' + shapePts.join(',') : styleShape,
'' + nodeHeight,
'' + nodeWidth
shape === 'polygon' ? shape + ',' + points.join(',') : shape,
'' + height,
'' + width
);

let cachedPath = pathCache[ key ];
let path;
let cacheHit = false;

if( cachedPath != null ){
path = cachedPath;
pathCacheHit = true;
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 = () => {
Expand All @@ -127,11 +150,11 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
}

r.nodeShapes[ r.getNodeShape( node ) ].draw(
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight );
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight );
}

if( usePaths ){
Expand Down Expand Up @@ -246,7 +269,133 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};

let drawOutline = () => {
if( outlineWidth > 0 ){
context.lineWidth = outlineWidth;
context.lineCap = 'butt';

if( context.setLineDash ){ // for very outofdate browsers
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 scaleX = (nodeWidth + borderWidth + (outlineWidth + outlineOffset)) / nodeWidth;
let scaleY = (nodeHeight + borderWidth + (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;
}

// draw the outline path, either by using expanded points or by scaling
// the dimensions, depending on shape
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 = (borderWidth + outlineOffset + outlineWidth) * 1.4;
} else if (shape === 'round-heptagon') {
sMult = (borderWidth + outlineOffset + outlineWidth) * 1.075;
offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 35;
} else if (shape === 'round-hexagon') {
sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12;
} else if (shape === 'round-pentagon') {
sMult = (borderWidth + outlineOffset + outlineWidth) * 1.13;
offsetY = -(borderWidth/2 + outlineOffset + outlineWidth) / 15;
} else if (shape === 'round-tag') {
sMult = (borderWidth + outlineOffset + outlineWidth) * 1.12;
offsetX = (borderWidth/2 + outlineWidth + outlineOffset) * .07;
} else if (shape === 'round-triangle') {
sMult = (borderWidth + outlineOffset + outlineWidth) * (Math.PI/2);
offsetY = -(borderWidth + outlineOffset/2 + outlineWidth) / Math.PI;
}

if (sMult !== 0) {
scaleX = (nodeWidth + sMult)/nodeWidth;
scaleY = (nodeHeight + sMult)/nodeHeight;
}

r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points);
} else if (['roundrectangle', 'round-rectangle'].includes(shape)) {
r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if (['cutrectangle', 'cut-rectangle'].includes(shape)) {
r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) {
r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight);
} 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 = (borderWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
} else {
let pad = (borderWidth + 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 = borderWidth / 3;

let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';

if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}

context.globalCompositeOperation = gco;
}

// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};

Expand Down Expand Up @@ -276,6 +425,8 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s

context.translate( gx, gy );

setupOutlineColor();
drawOutline();
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
Expand All @@ -296,13 +447,16 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s
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 ){
Expand Down
4 changes: 2 additions & 2 deletions src/style/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,9 @@ styfn.updateStyleHints = function(ele){
//

if( isNode ){
let { nodeBody, nodeBorder, backgroundImage, compound, pie } = _p.styleKeys;
let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie } = _p.styleKeys;

let nodeKeys = [ nodeBody, nodeBorder, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [
let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [
util.DEFAULT_HASH_SEED,
util.DEFAULT_HASH_SEED_ALT
]);
Expand Down
Loading
Loading