From 992de9a5ba26936ffc1598ae865ec7b66452c34b Mon Sep 17 00:00:00 2001 From: Shashank Shukla Date: Sat, 3 Jun 2023 20:31:39 +0530 Subject: [PATCH 1/3] New: Add support for label styling options --- docs/styles.md | 4 ++ examples/example-custom-styled-graph.html | 39 +++++++++++--- src/models/edge.ts | 4 ++ src/models/node.ts | 4 ++ src/renderer/canvas/edge/base.ts | 4 ++ src/renderer/canvas/label.ts | 63 +++++++++++++++++++---- src/renderer/canvas/node.ts | 4 ++ 7 files changed, 106 insertions(+), 16 deletions(-) diff --git a/docs/styles.md b/docs/styles.md index f59e93b..c1e9a30 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -33,6 +33,10 @@ the following properties: | `colorHover` | Color | string | Node background color on mouse hover event. If not defined `color` is used. | | `colorSelected` | Color | string | Node background color on mouse click event. If not defined `color` is used. | | `fontBackgroundColor` | Color | string | Node text (label) background color. | +| `fontBackgroundBorderWidth` | number | Node text (label) background border width. | +| `fontBackgroundBorderColor` | Color | string | Node text (label) background border color. | +| `fontBackgroundBorderRadius` | number | Node text (label) background border radius. | +| `fontBackgroundPadding` | number | Node text (label) background padding. | | `fontColor` | Color | string | Node text (label) font color. The default is `#000000`. | | `fontFamily` | string | Node text (label) font family. The default is `"Roboto, sans-serif"`. | | `fontSize` | number | Node text (label) font size. The default is `4`. | diff --git a/examples/example-custom-styled-graph.html b/examples/example-custom-styled-graph.html index 7339d25..574ae91 100644 --- a/examples/example-custom-styled-graph.html +++ b/examples/example-custom-styled-graph.html @@ -1,5 +1,6 @@ + @@ -7,11 +8,13 @@ +

Example 2 - Basic + Custom default style

@@ -30,7 +33,7 @@

Example 2 - Basic + Custom default style

const nodes = [ { id: 0, label: 'Node A' }, - { id: 1, label: 'Node B' }, + { id: 1, label: 'Node B - A Special Node' }, { id: 2, label: 'Node C' }, ]; const edges = [ @@ -63,12 +66,19 @@

Example 2 - Basic + Custom default style

size: 6, }; - if (node.data.label === 'Node A') { + if (node.data.label === 'Node B - A Special Node') { return { ...basicStyle, - size: 10, + size: 4, + borderWidth: 0.3, color: '#00FF2B', zIndex: 1, + fontBackgroundColor: 'rgb(0, 255, 0)', + fontBackgroundBorderWidth: 0.1, + fontBackgroundBorderColor: 'grey', + fontBackgroundBorderRadius: 2, + fontBackgroundPadding: 0.5, + fontSize: 2, }; } @@ -77,7 +87,7 @@

Example 2 - Basic + Custom default style

}; }, getEdgeStyle(edge) { - return { + const basicStyles = { color: '#999999', colorHover: '#1d1d1d', colorSelected: '#1d1d1d', @@ -86,7 +96,21 @@

Example 2 - Basic + Custom default style

widthHover: 0.9, widthSelected: 0.9, label: edge.data.label, - }; + } + + if (edge.data.label === 'A -> B') { + return { + ...basicStyles, + fontBackgroundColor: 'rgb(0, 255, 0)', + fontBackgroundBorderWidth: 0.1, + fontBackgroundBorderColor: 'grey', + fontBackgroundBorderRadius: 10, + fontBackgroundPadding: 0.5, + fontSize: 2, + } + } + + else return { ...basicStyles }; }, }); @@ -98,4 +122,5 @@

Example 2 - Basic + Custom default style

}); - + + \ No newline at end of file diff --git a/src/models/edge.ts b/src/models/edge.ts index 8fda65e..4a707d5 100644 --- a/src/models/edge.ts +++ b/src/models/edge.ts @@ -34,6 +34,10 @@ export type IEdgeStyle = Partial<{ colorHover: Color | string; colorSelected: Color | string; fontBackgroundColor: Color | string; + fontBackgroundBorderWidth: number; + fontBackgroundBorderColor: Color | string; + fontBackgroundBorderRadius: number; + fontBackgroundPadding: number; fontColor: Color | string; fontFamily: string; fontSize: number; diff --git a/src/models/node.ts b/src/models/node.ts index 320ddb6..8b835ac 100644 --- a/src/models/node.ts +++ b/src/models/node.ts @@ -45,6 +45,10 @@ export type INodeStyle = Partial<{ colorHover: Color | string; colorSelected: Color | string; fontBackgroundColor: Color | string; + fontBackgroundBorderWidth: number; + fontBackgroundBorderColor: Color | string; + fontBackgroundBorderRadius: number; + fontBackgroundPadding: number; fontColor: Color | string; fontFamily: string; fontSize: number; diff --git a/src/renderer/canvas/edge/base.ts b/src/renderer/canvas/edge/base.ts index f6fd2f5..9de7e46 100644 --- a/src/renderer/canvas/edge/base.ts +++ b/src/renderer/canvas/edge/base.ts @@ -57,6 +57,10 @@ const drawEdgeLabel = ( textBaseline: LabelTextBaseline.MIDDLE, properties: { fontBackgroundColor: edge.style.fontBackgroundColor, + fontBackgroundBorderWidth: edge.style.fontBackgroundBorderWidth, + fontBackgroundBorderColor: edge.style.fontBackgroundBorderColor, + fontBackgroundBorderRadius: edge.style.fontBackgroundBorderRadius, + fontBackgroundPadding: edge.style.fontBackgroundPadding, fontColor: edge.style.fontColor, fontFamily: edge.style.fontFamily, fontSize: edge.style.fontSize, diff --git a/src/renderer/canvas/label.ts b/src/renderer/canvas/label.ts index 78f0719..ef6a7b6 100644 --- a/src/renderer/canvas/label.ts +++ b/src/renderer/canvas/label.ts @@ -14,6 +14,10 @@ export enum LabelTextBaseline { export interface ILabelProperties { fontBackgroundColor: Color | string; + fontBackgroundBorderWidth: number; + fontBackgroundBorderColor: Color | string; + fontBackgroundBorderRadius: number; + fontBackgroundPadding: number; fontColor: Color | string; fontFamily: string; fontSize: number; @@ -75,19 +79,53 @@ const drawTextBackground = (context: CanvasRenderingContext2D, label: Label) => context.fillStyle = label.properties.fontBackgroundColor.toString(); const margin = label.fontSize * FONT_BACKGROUND_MARGIN; - const height = label.fontSize + 2 * margin; + + let height = label.fontSize + 2 * margin; const lineHeight = label.fontSize * FONT_LINE_SPACING; + const baselineHeight = label.textBaseline === LabelTextBaseline.MIDDLE ? label.fontSize / 2 : 0; for (let i = 0; i < label.textLines.length; i++) { const line = label.textLines[i]; - const width = context.measureText(line).width + 2 * margin; - context.fillRect( - label.position.x - width / 2, - label.position.y - baselineHeight - margin + i * lineHeight, - width, - height, - ); + + let width = context.measureText(line).width + 2 * margin; + let x = label.position.x - width / 2; + const y = label.position.y - baselineHeight - margin + i * lineHeight; + + if (label.properties.fontBackgroundPadding) { + width += label.properties.fontBackgroundPadding * 2; + height += label.properties.fontBackgroundPadding * 2; + x -= label.properties.fontBackgroundPadding; + } + + if (label.properties.fontBackgroundBorderRadius) { + const borderRadius = Math.min(label.properties.fontBackgroundBorderRadius, width / 2, height / 2); + + context.beginPath(); + context.lineWidth = label.properties.fontBackgroundBorderWidth ?? 0; + + context.moveTo(x + borderRadius, y); + context.lineTo(x + width - borderRadius, y); + context.quadraticCurveTo(x + width, y, x + width, y + borderRadius); + context.lineTo(x + width, y + height - borderRadius); + context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height); + context.lineTo(x + borderRadius, y + height); + context.quadraticCurveTo(x, y + height, x, y + height - borderRadius); + context.lineTo(x, y + borderRadius); + context.quadraticCurveTo(x, y, x + borderRadius, y); + context.closePath(); + context.fillStyle = (label.properties.fontBackgroundColor ?? DEFAULT_FONT_COLOR).toString(); + context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString(); + context.fill(); + context.stroke(); + } else if (label.properties.fontBackgroundBorderWidth && !label.properties.fontBackgroundBorderRadius) { + context.lineWidth = label.properties.fontBackgroundBorderWidth; + context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString(); + + context.strokeRect(x, y, width, height); + } else { + context.fillRect(x, y, width, height); + } } }; @@ -102,9 +140,16 @@ const drawText = (context: CanvasRenderingContext2D, label: Label) => { context.textAlign = 'center'; const lineHeight = label.fontSize * FONT_LINE_SPACING; + const x = label.position.x; + let y = label.position.y; + + if (label.properties.fontBackgroundPadding) { + y += label.properties.fontBackgroundPadding; + } + for (let i = 0; i < label.textLines.length; i++) { const line = label.textLines[i]; - context.fillText(line, label.position.x, label.position.y + i * lineHeight); + context.fillText(line, x, y + i * lineHeight); } }; diff --git a/src/renderer/canvas/node.ts b/src/renderer/canvas/node.ts index 814d7a6..b500a25 100644 --- a/src/renderer/canvas/node.ts +++ b/src/renderer/canvas/node.ts @@ -102,6 +102,10 @@ const drawNodeLabel = ( textBaseline: LabelTextBaseline.TOP, properties: { fontBackgroundColor: node.style.fontBackgroundColor, + fontBackgroundBorderWidth: node.style.fontBackgroundBorderWidth, + fontBackgroundBorderColor: node.style.fontBackgroundBorderColor, + fontBackgroundBorderRadius: node.style.fontBackgroundBorderRadius, + fontBackgroundPadding: node.style.fontBackgroundPadding, fontColor: node.style.fontColor, fontFamily: node.style.fontFamily, fontSize: node.style.fontSize, From 625d34e8dd98ecc6641708329a757762ead3d755 Mon Sep 17 00:00:00 2001 From: Shashank Shukla Date: Tue, 20 Jun 2023 19:13:52 +0530 Subject: [PATCH 2/3] Fix: Add fix to label background features --- examples/example-custom-styled-graph.html | 31 ++-- src/renderer/canvas/edge/base.ts | 2 +- src/renderer/canvas/label.ts | 187 ++++++++++++++++------ src/renderer/canvas/node.ts | 2 +- src/renderer/canvas/shapes.ts | 33 +++- 5 files changed, 187 insertions(+), 68 deletions(-) diff --git a/examples/example-custom-styled-graph.html b/examples/example-custom-styled-graph.html index 574ae91..b989f68 100644 --- a/examples/example-custom-styled-graph.html +++ b/examples/example-custom-styled-graph.html @@ -33,13 +33,13 @@

Example 2 - Basic + Custom default style

const nodes = [ { id: 0, label: 'Node A' }, - { id: 1, label: 'Node B - A Special Node' }, + { id: 1, label: 'Node B - A Special Node\nWith Some Special\nProperties' }, { id: 2, label: 'Node C' }, ]; const edges = [ { id: 0, start: 0, end: 0, label: 'A -> A' }, { id: 1, start: 0, end: 1, label: 'A -> B' }, - { id: 2, start: 0, end: 2, label: 'A -> C' }, + { id: 2, start: 0, end: 2, label: 'New Property\nA -> C' }, { id: 3, start: 1, end: 2, label: 'B -> C' }, { id: 4, start: 2, end: 2, label: 'C -> C' }, { id: 5, start: 0, end: 1, label: 'A -> B' }, @@ -57,7 +57,7 @@

Example 2 - Basic + Custom default style

getNodeStyle(node) { const basicStyle = { borderColor: '#1d1d1d', - borderWidth: 0.6, + borderWidth: 0.1, color: '#DD2222', colorHover: '#e7644e', colorSelected: '#e7644e', @@ -66,19 +66,20 @@

Example 2 - Basic + Custom default style

size: 6, }; - if (node.data.label === 'Node B - A Special Node') { + if (node.data.id === 1) { return { ...basicStyle, size: 4, borderWidth: 0.3, color: '#00FF2B', + borderColor: "purple", zIndex: 1, fontBackgroundColor: 'rgb(0, 255, 0)', - fontBackgroundBorderWidth: 0.1, - fontBackgroundBorderColor: 'grey', - fontBackgroundBorderRadius: 2, - fontBackgroundPadding: 0.5, - fontSize: 2, + fontBackgroundBorderWidth: 0.3, + fontBackgroundBorderColor: 'purple', + fontBackgroundBorderRadius: 1.5, + fontBackgroundPadding: 1, + fontSize: 1.5, }; } @@ -91,22 +92,22 @@

Example 2 - Basic + Custom default style

color: '#999999', colorHover: '#1d1d1d', colorSelected: '#1d1d1d', - fontSize: 3, + fontSize: 2, width: 0.3, widthHover: 0.9, widthSelected: 0.9, label: edge.data.label, } - if (edge.data.label === 'A -> B') { + if (edge.data.id === 2) { return { ...basicStyles, fontBackgroundColor: 'rgb(0, 255, 0)', - fontBackgroundBorderWidth: 0.1, + fontBackgroundBorderWidth: 0.2, fontBackgroundBorderColor: 'grey', fontBackgroundBorderRadius: 10, - fontBackgroundPadding: 0.5, - fontSize: 2, + fontBackgroundPadding: 1, + fontSize: 1.5, } } @@ -120,6 +121,8 @@

Example 2 - Basic + Custom default style

orb.view.render(() => { orb.view.recenter(); }); + + diff --git a/src/renderer/canvas/edge/base.ts b/src/renderer/canvas/edge/base.ts index 9de7e46..4d4338a 100644 --- a/src/renderer/canvas/edge/base.ts +++ b/src/renderer/canvas/edge/base.ts @@ -66,7 +66,7 @@ const drawEdgeLabel = ( fontSize: edge.style.fontSize, }, }); - drawLabel(context, label); + drawLabel(context, label, 'edge'); }; const drawLine = (context: CanvasRenderingContext2D, edge: IEdge) => { diff --git a/src/renderer/canvas/label.ts b/src/renderer/canvas/label.ts index ef6a7b6..f40b078 100644 --- a/src/renderer/canvas/label.ts +++ b/src/renderer/canvas/label.ts @@ -1,8 +1,11 @@ import { IPosition, Color } from '../../common'; +import { drawRoundRect } from './shapes'; const DEFAULT_FONT_FAMILY = 'Roboto, sans-serif'; const DEFAULT_FONT_SIZE = 4; const DEFAULT_FONT_COLOR = '#000000'; +const DEFAULT_FONT_BACKGROUND_COLOR = 'transparent'; +const DEFAULT_FONT_BORDER_COLOR = '#000000'; const FONT_BACKGROUND_MARGIN = 0.12; const FONT_LINE_SPACING = 1.2; @@ -29,6 +32,13 @@ export interface ILabelData { properties: Partial; } +type BorderPoint = { + x: number; + y: number; + width: number; + height: number; +}; + export class Label { public readonly text: string; public readonly textLines: string[] = []; @@ -62,74 +72,160 @@ export class Label { } } -export const drawLabel = (context: CanvasRenderingContext2D, label: Label) => { +export const drawLabel = (context: CanvasRenderingContext2D, label: Label, type: 'node' | 'edge') => { const isDrawable = label.textLines.length > 0 && label.fontSize > 0; if (!isDrawable || !label.position) { return; } - drawTextBackground(context, label); - drawText(context, label); + const borderPoints: BorderPoint[] | undefined = drawTextBackgroundAndBorder(context, label, type); + + if (!borderPoints) { + return; + } + + drawTextBackground(context, borderPoints, label); + drawTextBackgroundBorder(context, borderPoints, label); + drawText(context, borderPoints, label, type); }; -const drawTextBackground = (context: CanvasRenderingContext2D, label: Label) => { - if (!label.properties.fontBackgroundColor || !label.position) { +const drawTextBackgroundAndBorder = ( + context: CanvasRenderingContext2D, + label: Label, + type: 'node' | 'edge', +): BorderPoint[] | undefined => { + if (!label.position) { return; } - context.fillStyle = label.properties.fontBackgroundColor.toString(); const margin = label.fontSize * FONT_BACKGROUND_MARGIN; - let height = label.fontSize + 2 * margin; const lineHeight = label.fontSize * FONT_LINE_SPACING; const baselineHeight = label.textBaseline === LabelTextBaseline.MIDDLE ? label.fontSize / 2 : 0; + const borderPoints: BorderPoint[] = []; + for (let i = 0; i < label.textLines.length; i++) { const line = label.textLines[i]; let width = context.measureText(line).width + 2 * margin; let x = label.position.x - width / 2; - const y = label.position.y - baselineHeight - margin + i * lineHeight; + let y = label.position.y - baselineHeight - margin + i * lineHeight; + let height = label.fontSize + 2 * margin; if (label.properties.fontBackgroundPadding) { width += label.properties.fontBackgroundPadding * 2; height += label.properties.fontBackgroundPadding * 2; x -= label.properties.fontBackgroundPadding; + + if (type === 'node') { + y += label.properties.fontBackgroundPadding * 2 * i; + } else { + if (i === 0) { + y -= label.properties.fontBackgroundPadding; + } else { + y += label.properties.fontBackgroundPadding; + } + } } - if (label.properties.fontBackgroundBorderRadius) { - const borderRadius = Math.min(label.properties.fontBackgroundBorderRadius, width / 2, height / 2); - - context.beginPath(); - context.lineWidth = label.properties.fontBackgroundBorderWidth ?? 0; - - context.moveTo(x + borderRadius, y); - context.lineTo(x + width - borderRadius, y); - context.quadraticCurveTo(x + width, y, x + width, y + borderRadius); - context.lineTo(x + width, y + height - borderRadius); - context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height); - context.lineTo(x + borderRadius, y + height); - context.quadraticCurveTo(x, y + height, x, y + height - borderRadius); - context.lineTo(x, y + borderRadius); - context.quadraticCurveTo(x, y, x + borderRadius, y); - context.closePath(); - context.fillStyle = (label.properties.fontBackgroundColor ?? DEFAULT_FONT_COLOR).toString(); - context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString(); - context.fill(); - context.stroke(); - } else if (label.properties.fontBackgroundBorderWidth && !label.properties.fontBackgroundBorderRadius) { - context.lineWidth = label.properties.fontBackgroundBorderWidth; - context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString(); - - context.strokeRect(x, y, width, height); - } else { - context.fillRect(x, y, width, height); + borderPoints.push({ x, y, width, height }); + } + + return borderPoints; +}; + +const calculateBorderRadius = (borderRadius: number, width: number, height: number) => { + // ensure that the radius isn't too large for x + if (width - 2 * borderRadius < 0) { + borderRadius = width / 2; + } + + // ensure that the radius isn't too large for y + if (height - 2 * borderRadius < 0) { + borderRadius = height / 2; + } + + return borderRadius; +}; + +const drawTextBackground = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label) => { + if (!label.properties.fontBackgroundColor) { + return; + } + + const backgroundColor = label.properties.fontBackgroundColor ?? DEFAULT_FONT_BACKGROUND_COLOR; + const fontBorderRadius = label.properties.fontBackgroundBorderRadius ?? 0; + + context.fillStyle = backgroundColor.toString(); + + for (let i = 0; i < borderPoints.length; i++) { + const { x, y, width, height } = borderPoints[i]; + const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); + drawRoundRect(context, x, y, width, height, borderRadius, i === 0, true); + context.fill(); + } +}; + +const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label) => { + if (!label.properties.fontBackgroundBorderWidth) { + return; + } + + const borderWidth = label.properties.fontBackgroundBorderWidth; + const borderColor = label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_BORDER_COLOR; + const fontBorderRadius = label.properties.fontBackgroundBorderRadius ?? 0; + context.lineWidth = borderWidth; + context.strokeStyle = borderColor.toString(); + + context.beginPath(); + + if (borderPoints.length === 1) { + const { x, y, height, width } = borderPoints[0]; + const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); + drawRoundRect(context, x, y, width, height, borderRadius); + } else { + for (let i = 0; i < borderPoints.length; i++) { + const { x, y, height, width } = borderPoints[i]; + const { x: nextX, width: nextWidth } = i !== borderPoints.length - 1 ? borderPoints[i + 1] : { x: 0, width: 0 }; + const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); + if (i === 0) { + context.moveTo(x + borderRadius, y); + context.lineTo(x + width - borderRadius, y); + context.quadraticCurveTo(x + width, y, x + width, y + borderRadius); + context.lineTo(x + width, y + height - borderRadius); + context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height); + context.lineTo(nextX + nextWidth, y + height); + context.moveTo(x + borderRadius, y); + context.quadraticCurveTo(x, y, x, y + borderRadius); + context.lineTo(x, y + height - borderRadius); + context.quadraticCurveTo(x, y + height, x + borderRadius, y + height); + context.lineTo(nextX, y + height); + } else if (i === borderPoints.length - 1) { + context.moveTo(x + width, y); + context.lineTo(x + width, y + height - borderRadius); + context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height); + context.lineTo(x + borderRadius, y + height); + context.quadraticCurveTo(x, y + height, x, y + height - borderRadius); + context.lineTo(x, y); + } else { + context.moveTo(x + width, y); + context.lineTo(x + width, y + height - borderRadius); + context.quadraticCurveTo(x + width, y + height, x + width - borderRadius, y + height); + context.lineTo(nextX + nextWidth, y + height); + context.moveTo(x, y); + context.lineTo(x, y + height - borderRadius); + context.quadraticCurveTo(x, y + height, x + borderRadius, y + height); + context.lineTo(nextX, y + height); + } } } + + context.stroke(); }; -const drawText = (context: CanvasRenderingContext2D, label: Label) => { +const drawText = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label, type: String) => { if (!label.position) { return; } @@ -138,18 +234,19 @@ const drawText = (context: CanvasRenderingContext2D, label: Label) => { context.font = label.fontFamily; context.textBaseline = label.textBaseline; context.textAlign = 'center'; - const lineHeight = label.fontSize * FONT_LINE_SPACING; - - const x = label.position.x; - let y = label.position.y; - if (label.properties.fontBackgroundPadding) { - y += label.properties.fontBackgroundPadding; + if (label.textLines.includes('A -> B')) { + console.log(borderPoints, label); } - for (let i = 0; i < label.textLines.length; i++) { - const line = label.textLines[i]; - context.fillText(line, x, y + i * lineHeight); + for (let i = 0; i < borderPoints.length; i++) { + const { x, y, width, height } = borderPoints[i]; + const textLine = label.textLines[i]; + if (type === 'node') { + context.fillText(textLine, x + width / 2, y + (label.properties.fontBackgroundPadding ?? 0)); + } else { + context.fillText(textLine, x + width / 2, y + height / 2); + } } }; diff --git a/src/renderer/canvas/node.ts b/src/renderer/canvas/node.ts index b500a25..726de4f 100644 --- a/src/renderer/canvas/node.ts +++ b/src/renderer/canvas/node.ts @@ -111,7 +111,7 @@ const drawNodeLabel = ( fontSize: node.style.fontSize, }, }); - drawLabel(context, label); + drawLabel(context, label, 'node'); }; const drawImage = ( diff --git a/src/renderer/canvas/shapes.ts b/src/renderer/canvas/shapes.ts index 64ae4dc..4bac310 100644 --- a/src/renderer/canvas/shapes.ts +++ b/src/renderer/canvas/shapes.ts @@ -143,6 +143,8 @@ export const drawDiamond = (context: CanvasRenderingContext2D, x: number, y: num * @param {number} w Width * @param {number} h Height * @param {number} r Border radius + * @param {boolean} [roundedTop=true] Whether the top corners should be rounded + * @param {boolean} [roundedBottom=true] Whether the bottom corners should be rounded */ export const drawRoundRect = ( context: CanvasRenderingContext2D, @@ -151,6 +153,8 @@ export const drawRoundRect = ( w: number, h: number, r: number, + roundedTop: Boolean = true, + roundedBottom: Boolean = true, ) => { const r2d = Math.PI / 180; @@ -165,15 +169,30 @@ export const drawRoundRect = ( } context.beginPath(); - context.moveTo(x + r, y); - context.lineTo(x + w - r, y); - context.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); - context.lineTo(x + w, y + h - r); - context.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); + if (roundedTop) { + context.moveTo(x + r, y); + context.lineTo(x + w - r, y); + context.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); + } else { + context.moveTo(x, y); + context.lineTo(x + w, y); + } + if (roundedBottom) { + context.lineTo(x + w, y + h - r); + context.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); + } else { + context.lineTo(x + w, y + h); + } context.lineTo(x + r, y + h); context.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); - context.lineTo(x, y + r); - context.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); + + if (roundedTop) { + context.lineTo(x, y + r); + context.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); + } else { + context.lineTo(x, y); + } + context.closePath(); }; From 637dfb2931160cd53d304196c413eff0e6f4065a Mon Sep 17 00:00:00 2001 From: Shashank Shukla Date: Tue, 25 Jul 2023 21:22:29 +0530 Subject: [PATCH 3/3] Fix: removed unnecessary variables and parameters --- src/renderer/canvas/edge/base.ts | 2 +- src/renderer/canvas/label.ts | 66 +++++++++----------------------- src/renderer/canvas/node.ts | 2 +- src/renderer/canvas/shapes.ts | 45 ++++++++++++++++++---- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/renderer/canvas/edge/base.ts b/src/renderer/canvas/edge/base.ts index 4d4338a..9de7e46 100644 --- a/src/renderer/canvas/edge/base.ts +++ b/src/renderer/canvas/edge/base.ts @@ -66,7 +66,7 @@ const drawEdgeLabel = ( fontSize: edge.style.fontSize, }, }); - drawLabel(context, label, 'edge'); + drawLabel(context, label); }; const drawLine = (context: CanvasRenderingContext2D, edge: IEdge) => { diff --git a/src/renderer/canvas/label.ts b/src/renderer/canvas/label.ts index f40b078..c3c8438 100644 --- a/src/renderer/canvas/label.ts +++ b/src/renderer/canvas/label.ts @@ -1,10 +1,9 @@ -import { IPosition, Color } from '../../common'; -import { drawRoundRect } from './shapes'; +import { IPosition, Color, IRectangle } from '../../common'; +import { drawRoundRect, getMaxValidBorderRadius } from './shapes'; const DEFAULT_FONT_FAMILY = 'Roboto, sans-serif'; const DEFAULT_FONT_SIZE = 4; const DEFAULT_FONT_COLOR = '#000000'; -const DEFAULT_FONT_BACKGROUND_COLOR = 'transparent'; const DEFAULT_FONT_BORDER_COLOR = '#000000'; const FONT_BACKGROUND_MARGIN = 0.12; @@ -32,13 +31,6 @@ export interface ILabelData { properties: Partial; } -type BorderPoint = { - x: number; - y: number; - width: number; - height: number; -}; - export class Label { public readonly text: string; public readonly textLines: string[] = []; @@ -72,13 +64,13 @@ export class Label { } } -export const drawLabel = (context: CanvasRenderingContext2D, label: Label, type: 'node' | 'edge') => { +export const drawLabel = (context: CanvasRenderingContext2D, label: Label) => { const isDrawable = label.textLines.length > 0 && label.fontSize > 0; if (!isDrawable || !label.position) { return; } - const borderPoints: BorderPoint[] | undefined = drawTextBackgroundAndBorder(context, label, type); + const borderPoints: IRectangle[] | undefined = getLabelBoundaryPoints(context, label); if (!borderPoints) { return; @@ -86,14 +78,10 @@ export const drawLabel = (context: CanvasRenderingContext2D, label: Label, type: drawTextBackground(context, borderPoints, label); drawTextBackgroundBorder(context, borderPoints, label); - drawText(context, borderPoints, label, type); + drawText(context, borderPoints, label); }; -const drawTextBackgroundAndBorder = ( - context: CanvasRenderingContext2D, - label: Label, - type: 'node' | 'edge', -): BorderPoint[] | undefined => { +const getLabelBoundaryPoints = (context: CanvasRenderingContext2D, label: Label): IRectangle[] | undefined => { if (!label.position) { return; } @@ -104,11 +92,12 @@ const drawTextBackgroundAndBorder = ( const baselineHeight = label.textBaseline === LabelTextBaseline.MIDDLE ? label.fontSize / 2 : 0; - const borderPoints: BorderPoint[] = []; + const borderPoints: IRectangle[] = []; for (let i = 0; i < label.textLines.length; i++) { const line = label.textLines[i]; + context.font = label.fontFamily; let width = context.measureText(line).width + 2 * margin; let x = label.position.x - width / 2; let y = label.position.y - baselineHeight - margin + i * lineHeight; @@ -119,7 +108,7 @@ const drawTextBackgroundAndBorder = ( height += label.properties.fontBackgroundPadding * 2; x -= label.properties.fontBackgroundPadding; - if (type === 'node') { + if (label.textBaseline === LabelTextBaseline.TOP) { y += label.properties.fontBackgroundPadding * 2 * i; } else { if (i === 0) { @@ -136,39 +125,25 @@ const drawTextBackgroundAndBorder = ( return borderPoints; }; -const calculateBorderRadius = (borderRadius: number, width: number, height: number) => { - // ensure that the radius isn't too large for x - if (width - 2 * borderRadius < 0) { - borderRadius = width / 2; - } - - // ensure that the radius isn't too large for y - if (height - 2 * borderRadius < 0) { - borderRadius = height / 2; - } - - return borderRadius; -}; - -const drawTextBackground = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label) => { +const drawTextBackground = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => { if (!label.properties.fontBackgroundColor) { return; } - const backgroundColor = label.properties.fontBackgroundColor ?? DEFAULT_FONT_BACKGROUND_COLOR; + const backgroundColor = label.properties.fontBackgroundColor; const fontBorderRadius = label.properties.fontBackgroundBorderRadius ?? 0; context.fillStyle = backgroundColor.toString(); for (let i = 0; i < borderPoints.length; i++) { const { x, y, width, height } = borderPoints[i]; - const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); - drawRoundRect(context, x, y, width, height, borderRadius, i === 0, true); + const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height); + drawRoundRect(context, x, y, width, height, borderRadius, { isTopRounded: i === 0, isBottomRounded: true }); context.fill(); } }; -const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label) => { +const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => { if (!label.properties.fontBackgroundBorderWidth) { return; } @@ -183,13 +158,14 @@ const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoint if (borderPoints.length === 1) { const { x, y, height, width } = borderPoints[0]; - const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); + const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height); drawRoundRect(context, x, y, width, height, borderRadius); } else { for (let i = 0; i < borderPoints.length; i++) { const { x, y, height, width } = borderPoints[i]; const { x: nextX, width: nextWidth } = i !== borderPoints.length - 1 ? borderPoints[i + 1] : { x: 0, width: 0 }; - const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); + const borderRadius = getMaxValidBorderRadius(fontBorderRadius, width, height); + if (i === 0) { context.moveTo(x + borderRadius, y); context.lineTo(x + width - borderRadius, y); @@ -225,7 +201,7 @@ const drawTextBackgroundBorder = (context: CanvasRenderingContext2D, borderPoint context.stroke(); }; -const drawText = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[], label: Label, type: String) => { +const drawText = (context: CanvasRenderingContext2D, borderPoints: IRectangle[], label: Label) => { if (!label.position) { return; } @@ -235,14 +211,10 @@ const drawText = (context: CanvasRenderingContext2D, borderPoints: BorderPoint[] context.textBaseline = label.textBaseline; context.textAlign = 'center'; - if (label.textLines.includes('A -> B')) { - console.log(borderPoints, label); - } - for (let i = 0; i < borderPoints.length; i++) { const { x, y, width, height } = borderPoints[i]; const textLine = label.textLines[i]; - if (type === 'node') { + if (label.textBaseline === LabelTextBaseline.TOP) { context.fillText(textLine, x + width / 2, y + (label.properties.fontBackgroundPadding ?? 0)); } else { context.fillText(textLine, x + width / 2, y + height / 2); diff --git a/src/renderer/canvas/node.ts b/src/renderer/canvas/node.ts index 726de4f..b500a25 100644 --- a/src/renderer/canvas/node.ts +++ b/src/renderer/canvas/node.ts @@ -111,7 +111,7 @@ const drawNodeLabel = ( fontSize: node.style.fontSize, }, }); - drawLabel(context, label, 'node'); + drawLabel(context, label); }; const drawImage = ( diff --git a/src/renderer/canvas/shapes.ts b/src/renderer/canvas/shapes.ts index 4bac310..7615bce 100644 --- a/src/renderer/canvas/shapes.ts +++ b/src/renderer/canvas/shapes.ts @@ -132,6 +132,16 @@ export const drawDiamond = (context: CanvasRenderingContext2D, x: number, y: num context.closePath(); }; +export interface IRoundRectOptions { + isTopRounded: boolean; + isBottomRounded: boolean; +} + +const DEFAULT_ROUND_RECT_OPTIONS: IRoundRectOptions = { + isTopRounded: true, + isBottomRounded: true, +}; + /** * Draws a rounded rectangle. * @see {@link https://github.com/almende/vis/blob/master/lib/network/shapes.js} @@ -143,8 +153,7 @@ export const drawDiamond = (context: CanvasRenderingContext2D, x: number, y: num * @param {number} w Width * @param {number} h Height * @param {number} r Border radius - * @param {boolean} [roundedTop=true] Whether the top corners should be rounded - * @param {boolean} [roundedBottom=true] Whether the bottom corners should be rounded + * @param {IRoundRectOptions} options */ export const drawRoundRect = ( context: CanvasRenderingContext2D, @@ -153,9 +162,10 @@ export const drawRoundRect = ( w: number, h: number, r: number, - roundedTop: Boolean = true, - roundedBottom: Boolean = true, + options?: Partial, ) => { + const config = { ...DEFAULT_ROUND_RECT_OPTIONS, ...options }; + const r2d = Math.PI / 180; // ensure that the radius isn't too large for x @@ -169,7 +179,7 @@ export const drawRoundRect = ( } context.beginPath(); - if (roundedTop) { + if (config.isTopRounded) { context.moveTo(x + r, y); context.lineTo(x + w - r, y); context.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); @@ -177,7 +187,7 @@ export const drawRoundRect = ( context.moveTo(x, y); context.lineTo(x + w, y); } - if (roundedBottom) { + if (config.isBottomRounded) { context.lineTo(x + w, y + h - r); context.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); } else { @@ -186,7 +196,7 @@ export const drawRoundRect = ( context.lineTo(x + r, y + h); context.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); - if (roundedTop) { + if (config.isTopRounded) { context.lineTo(x, y + r); context.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); } else { @@ -257,3 +267,24 @@ export const drawNgon = (context: CanvasRenderingContext2D, x: number, y: number context.closePath(); }; + +/** + * + * @param {Number} borderRadius Border radius provided by the user + * @param {Number} width width of the container surrounding the label + * @param {Number} height height of the container surrounding the label + * @return {Number} The maximum valid border radius + */ +export const getMaxValidBorderRadius = (borderRadius: number, width: number, height: number) => { + // ensure that the radius isn't too large for x + if (width - 2 * borderRadius < 0) { + borderRadius = width / 2; + } + + // ensure that the radius isn't too large for y + if (height - 2 * borderRadius < 0) { + borderRadius = height / 2; + } + + return borderRadius; +};