-
Notifications
You must be signed in to change notification settings - Fork 18
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
New: Add support for label styling options #63
base: release/0.5.0
Are you sure you want to change the base?
New: Add support for label styling options #63
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is one additional bug. When using multi-line labels, there is no background. Just change the line 36 in the example-custom-styled-graph
to be:
{ id: 1, label: 'Node B - A Special Node\nNew Line' },
And you will get this:
The expected behavior is to have a background under each line.
And now comes an additional question which is how border color behaves when you have one line background touching the background from the line below (e.g. padding is larger than the default line spacing) because there can be two states: A or B. What we want, is the B.
src/renderer/canvas/label.ts
Outdated
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can use drawRoundRect
from shapes
here.
src/renderer/canvas/label.ts
Outdated
context.lineTo(x, y + borderRadius); | ||
context.quadraticCurveTo(x, y, x + borderRadius, y); | ||
context.closePath(); | ||
context.fillStyle = (label.properties.fontBackgroundColor ?? DEFAULT_FONT_COLOR).toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure you want to use DEFAULT_FONT_COLOR
here? If default font color is black, and the background is black too, then by default, the text won't be visible.
src/renderer/canvas/label.ts
Outdated
context.strokeStyle = (label.properties.fontBackgroundBorderColor ?? DEFAULT_FONT_COLOR).toString(); | ||
context.fill(); | ||
context.stroke(); | ||
} else if (label.properties.fontBackgroundBorderWidth && !label.properties.fontBackgroundBorderRadius) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already have if(label.properties.fontBackgroundBorderRadius)
on line 101, to the check for (!label.properties.fontBackgroundBorderRadius` is not needed here, it is irrelevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is hard to figure out what will be the shape drawn from these two ifs and else so I propose splitting it so it is much easier to understand what final shape will be drawn, e.g.
// setup and preps
const isBackgroundShapeDrawn = ....
if (!isBackgroundShapeDrawn) {
continue;
}
// I think this function will also handle radius = 0 to draw proper rect
drawRoundRect(...);
if (backgroundColor) {
setFill =
fill();
}
If (borderWidth) {
setStrokeStyle =
stroke();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks again for this feature request. It is a really cool feature. I've added several comments because I think we can remove some parts of the code and still do the same thing - which is easier to extend later on in the future.
Let me know what do you think of the comments.
src/renderer/canvas/shapes.ts
Outdated
roundedTop: Boolean = true, | ||
roundedBottom: Boolean = true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw when there are optional params like this, it is good to combine them into a Partial
object. It is easier to extend + the code is more readable (especially when calling it), e.g.
export interface IRoundRectOptions {
isTopRounded: boolean;
isBottomRounded: boolean;
}
const DEFAULT_ROUND_RECT_OPTIONS: IRoundRectOptions = {
isTopRounded: true;
isBottomRounded: true;
};
export const drawRoundRect = (..... r: number, options?: Partial<IRoundRectOptions>) {
const config = { ...DEFAULT_ROUND_RECT_OPTIONS, ...options };
...
if (config.isTopRounded) { ... }
}
And then when using this, you have the context of the object which is much easier to read:
// Before (rounded top = true)
drawRoundRect(context, 100, 100, 20, false)
// Before (if you just want to have rounded bottom = false, you still need to provide rounded top
drawRoundRect(context, 100, 100, 20, true, false)
// ---
// After, when calling a function, you already know what option you are overriding
drawRoundRect(context, 100, 100, 20, { isTopRounded: false });
// After, you can also just override the option you want to without noting all the previous params
drawRoundRect(context, 100, 100, 20, { isBottomRounded: false });
drawRoundRect(context, 100, 100, 20, { isTopRounded: false, isBottomRounded: false });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/renderer/canvas/label.ts
Outdated
@@ -58,40 +72,160 @@ export class Label { | |||
} | |||
} | |||
|
|||
export const drawLabel = (context: CanvasRenderingContext2D, label: Label) => { | |||
export const drawLabel = (context: CanvasRenderingContext2D, label: Label, type: 'node' | 'edge') => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really get why you need information about if it is a node
or edge
. Why does it matter?
Both can have multiple lines of text. As far as I can see, you probably wanted to handle the situation where padding is larger than the initial margin and in the node label case, the background color of the text would go over/under the node circle, right?
That's why we have this textBaseline
property. Nodes have that set as TOP
, and edges have it as MIDDLE
. With that info, you have everything you need. No need to send 'node'
and/or 'edge'
.
src/renderer/canvas/edge/base.ts
Outdated
fontColor: edge.style.fontColor, | ||
fontFamily: edge.style.fontFamily, | ||
fontSize: edge.style.fontSize, | ||
}, | ||
}); | ||
drawLabel(context, label); | ||
drawLabel(context, label, 'edge'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check that one other comment, this 'edge'
part can be removed.
src/renderer/canvas/node.ts
Outdated
fontColor: node.style.fontColor, | ||
fontFamily: node.style.fontFamily, | ||
fontSize: node.style.fontSize, | ||
}, | ||
}); | ||
drawLabel(context, label); | ||
drawLabel(context, label, 'node'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check that one other comment, this 'node'
part can be removed.
src/renderer/canvas/label.ts
Outdated
if (label.textLines.includes('A -> B')) { | ||
console.log(borderPoints, label); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be removed. You probably used it for testing.
src/renderer/canvas/label.ts
Outdated
}; | ||
|
||
const drawTextBackground = (context: CanvasRenderingContext2D, label: Label) => { | ||
if (!label.properties.fontBackgroundColor || !label.position) { | ||
const drawTextBackgroundAndBorder = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of this function and what it does doesn't match. It tells that it is doing the drawing, but it is actually getting the points for the background of the text.
When I get into it much deeper, those are actually bounding boxes rather than border points. But this is nitpicking.
src/renderer/canvas/label.ts
Outdated
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; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a cool function that could go to shapes.ts
utility file, like getMaxValidBorderRadius
.
const borderRadius = calculateBorderRadius(fontBorderRadius, width, height); | ||
drawRoundRect(context, x, y, width, height, borderRadius); | ||
} else { | ||
for (let i = 0; i < borderPoints.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not 100% sure why you need this logic here. I feel like all this can be done using 3 functions which are: drawRoundRect
+ context.fill()
and context.stroke()
.
Here is the idea:
- There are two stages: draw background and draw foreground (the actual text)
- Stage 1: Background
- baseline height in case if text baseline is
TOP
should incorporate the padding (so it doesn't overlay what is above it, e.g. node circle) - for each line of text get the bounding boxes (what you called BorderPoints) - check the
context.font
forcontext.measureText
(it is in another comment) - for each line call
drawRoundRect
- what is cool is that it will make a path and it will not fill nor stroke it - which is great - after all lines are called with
drawRoundRect
we can just do 2 calls:context.stroke()
(if backgroundBorderWidth > 0) andcontext.fill()
- this fill will override the overlapping strokes.
- baseline height in case if text baseline is
You can test this last piece with this little sample:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Canvas</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="tutorial" width="550" height="550"></canvas>
<script>
function draw() {
const canvas = document.getElementById("tutorial");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
ctx.fillStyle = 'yellow';
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.rect(100, 100, 20, 20);
ctx.rect(110, 110, 20, 20);
ctx.stroke();
ctx.fill();
}
}
draw();
</script>
</body>
</html>
- Stage 2: Foreground
- Everything should stay the same as before, the only difference is the starting
y
when the text baseline isTOP
(in the case of the nodes) because of the padding - so the baseline height needs to incorporate the padding.
- Everything should stay the same as before, the only difference is the starting
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there will be one issue here if there is no backgroundColor i.e. label.properties.fontBackgroundColor
is not set, in this case with the above method, we will get overlapping borders around the label. meaning if we don't apply context.fill()
then it will look something like this.
So, the border has to be drawn independently of the background.
Please let me know if my understanding is not correct.
src/renderer/canvas/label.ts
Outdated
@@ -25,6 +32,13 @@ export interface ILabelData { | |||
properties: Partial<ILabelProperties>; | |||
} | |||
|
|||
type BorderPoint = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, we do all interfaces and types with the prefix I
so we can differentiate it from the classes with the same name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, you already have a type you need, it is called IRectangle
from common/rectangle.ts
. Same fields, same types.
This PR will add support to add label background styling options.
This will add style properties -