-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Node outlines #3158
Conversation
Is the rhomboid example intended to be offset or flush with the node shape? How do more complex shapes, like star, fare? |
The rhomboid example was with an 8px offset (edited the caption to that effect) but it should definitely have a consistent offset on all sides, as all outlines should. To your question, rhomboid turns out to be just one of a handful of irregular shapes that aren't being outlined consistently, and where doing so looks to be less straightforward than expected. I'm going to work on revisiting the rendering logic with some of those cases in mind. I'll be away next week but hope to follow-up soon with an update. |
I see. Other shapes to watch out for may include One way to simplify the implementation would be to drop support for doubled outlines and Looking forward to your updates. |
Thanks @maxkfranz. I've managed to get some better results by massaging the node width/height/offset for some of the irregular shapes: (4px outline w/ 2px offset) Not sold on the approach though as it feels somewhat brittle, and I can't say there's much rhyme or reason to the values I've settled on. Also, rhomboids are still problematic. Here's what some of that looks like though: let multiplier = outlineWidth / 2 + outlineOffset * 2 + borderWidth;
if (shape === "triangle" || shape === "round-triangle") {
multiplier = outlineWidth + outlineOffset + borderWidth;
npos.y = -multiplier * .3;
width += multiplier * .6;
height += multiplier * .6;
} else if (shape === "vee") {
npos.y = -multiplier / 2 * .3;
width += multiplier / 2 * 3;
height += multiplier / 2 * 3;
} else if (shape === "tag" || shape === "round-tag") {
npos.x = multiplier * .1;
width += multiplier * .2;
} else if (shape === "star") {
npos.y = -multiplier * .15;
width += multiplier * .9;
height += multiplier * .9;
} else if (shape === "diamond" || shape === "round-diamond") {
multiplier = outlineWidth/2 + outlineOffset * 2 + borderWidth/2;
width += multiplier * .66;
height += multiplier * .66;
} else if (shape === "pentagon" || shape === "round-pentagon") {
npos.y = -multiplier * .1;
width += multiplier * .2;
height += multiplier * .2;
} else if (shape == "heptagon" || shape == "round-heptagon") {
npos.y = -multiplier * .03;
width += multiplier * .03;
height += multiplier * .03;
} Noting that I've seen some unexpected rendering anomalies when using larger line-widths, but that seems to occur with borders as well. The inner edge of outlines for rounded shapes don't line up with the corresponding border segment as can be seen above, but I'm not sure that's something we can do much about (?):
I'm open to exploring this path. Can you go into a bit more detail about what Canvas operations you see this approach using, and/or posting some basic pseudo-code? |
For solid outlines: Reuse the border code. Double the border width. The border is centred on the shape, so half is inside and half is outside. Clip the doubled border you’ve drawn with the node shape to remove the inside portion. |
Thanks @maxkfranz. Went ahead and dropped support for the 4px solid outline 1px solid outline Ended up using Let me know if anything stands out. |
That can essentially be a cheaper method of clipping. Great.
It looks solid. I'll review in more depth later this week. Let's get this merged. |
@@ -114,7 +122,7 @@ CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, s | |||
} | |||
} | |||
|
|||
let drawShape = () => { | |||
let drawPath = () => { |
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.
Not a huge deal but this function should probably be named differently, especially since the core render methods are prefixed with the draw
prefix. Maybe something like ensurePath
would be more accurate semantically?
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.
Sounds good
} | ||
|
||
const gco = context.globalCompositeOperation; | ||
context.globalCompositeOperation = 'destination-out'; |
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.
Do we need to worry about what happens when an outline renders over another already rendered element here? Wouldn't it clip the outline above that too?
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 was thinking about that too the other day. The GCO is fine for things like edge arrows, since they're so small. However, there are important use cases for having semitransparent nodes -- e.g. with overlap -- and the GCO would visually delete things.
It would be best to use a clip operation here. It can be a bit cheaper if you use the nodePathCache
for the clipping rather than having to redraw/regenerate the node body.
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've managed to update the rendering to clip instead of use gco, but only by rendering a rect with the node shape cut out, and clipping to that. I might very well be missing something, but clipping to the node shape has the opposite effect of clipping the outer portion of the outline instead of the inside portion.
There are still a bunch of irregular shapes whose outline exceeds the node width/height + outline width though, and I'm not sure how to accurately expand the rect bounds in those cases, and when calculating the overall node bounds in bounds.js
if an outline is present. I can apply an arbitrary multiplier, but that feels like a hack which would likely have adverse implications on node layout, etc.
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.
That's a good point. Here are some initial thoughts:
(1) Two multipliers, one for width and one for height, might suffice for predefined shapes. One possibility is something like bb.w = k_w * outlineWidth + otherMeasuresForWidth
.
(2) Assuming we go with (1), how would we address custom polygon
shapes? This leads into (3). Alternatively, we could lean on the existing bounds-expansion
property. The dev, in that case, must specify their own expansion of the bounds to accommodate the outline on their custom polygon.
(3) The proper way to calculate the adjusted bounding box would be to take the intersection of the relevant lines in the shape, when they're pushed out by the outline width. This would be more expensive, and it would mean that we may as well use a path for the outline, since we'd have the points to form the path anyway.
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.
Taking a step back, pretty much all of the complexity that's arisen has come with trying to solve the use case of the background and/or border having a partial opacity, and not wanting any of the outline below to show through.
But we already have that with borders, where a border with partial opacity effectively creates two visual borders due to the fact that the inner half overlaps the node shape:
In an ideal world, it might be worth considering rendering both borders and outlines such that they never overlapped other elements. In practice though this has proven to be less straightforward than it would seem, whether accomplished by offsetting the stroke or by clipping, and there's a chance that the computational cost would end up outweighing the aesthetics, making it hard to justify the tradeoff.
While adding outlines admittedly moves us closer to that potential decision point, I wonder if we could/should consider simplifying the outline implementation for now by not worrying about clipping the inner portion, and consider that broader rethink around how strokes are rendered (whether border or outline) down the road.
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.
What if we rebrand the feature?
What you're describing sounds like a under-border
rather than an outline
. That way, you can still use it as an outline -- with the opacity caveats etc. -- but it would have clear expectations for other devs who use the feature. Nobody would be disappointed about the transparent node use cases, since it's obvious that those use cases aren't suited to an under-border.
The implementation would be identical to the existing borders, apart from the draw order.
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.
@maxkfranz that could be a possibility as a fallback, but my preference would be to keep the outline framing if possible. I will give the suggestion you proposed above (calculating the path by pushing out the points by the outline width) a shot.
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.
Sounds good
This issue has been automatically marked as stale, because it has not had activity within the past 14 days. It will be closed if no further activity occurs within the next 7 days. If a feature request is important to you, please consider making a pull request. Thank you for your contributions. |
Pinned |
As long as it works in practice, it’s fine. It may be necessary to use different multipliers for different shapes, and different multipliers for x versus y. We don’t want the bounds to be too far off from reality.
Instead of aiming for perfection here, how about we just use the implementation for the non-round shapes for the outlines of the corresponding rounded shapes? Most users probably wouldn’t notice the difference for many outline use cases, anyway. It could always be improved in future, if necessary. How does that sound? |
I updated the round shape outline rendering logic to apply a scale multiplier and slight offset where necessary to get them more in line with the other shapes, and tweaked the bounds calculation to use a standard formula with a few exceptions where needed. Feel pretty good about it now all considering, so @maxkfranz, please take a good look when you get a chance and let me know if anything stands out. |
Great. I'll test it out shortly. Thanks, @npedrini |
@npedrini, I tested out the PR and overall it looks great. I put some changes to the debug page for testing in this branch: When you run There are a number of helpful tools in the right sidebar. For this feature, the 'Selected BB' (bounding box) buttons are useful. Just select a node and then click one of the buttons to see its bounding box. One thing that could be improved in this PR is to tighten up the bounding box adjustments a bit. What does your formula look like for the multipliers? For some shapes especially, the bounding box seems a bit loose. See the video below: Screen.Recording.2023-10-26.at.10.36.21.mov |
Thanks @maxkfranz, the bounds tool in the debug mode was indeed useful in getting a visual indication of what the bounds were looking like. I've tightened them up a bit and they're looking more consistent to me: Screen.Recording.2023-10-27.at.4.47.40.PM.movFor most shapes I'm calculating an x- and y-scale with outline and offset relative to the node bounds, then expanding the bounds by the pixel equivalent of the delta in either direction. For some of the more irregular shapes I'm adjusting those scales with arbitrary multipliers that seem to achieve the desired results. I've been testing that with small outline settings like you've added in that debug branch, as well as exaggerated values. Do we need to update the { name: 'outline-offset', type: t.size, triggersBounds: diff.any } |
Looking good. Re. bounds triggering, yes. Anything that could alter the bounds when the prop is changed needs to have a diff set. For continuous values, generally 'any' is appropriate -- as you suggested. |
@npedrini @danprince, let me know if there's anything else you'd like to consider in this PR. Otherwise, let's merge this in. |
@maxkfranz Don't think so, looking great on our end! |
Good stuff. Merged |
Associated issues: #3156
Adds node outlines as described in #3156. Pushed this one up a bit early, but will be monitoring the associated issue and integrate any feedback that comes in.
Reiterating the feature as laid out in the issue description, node outlines support the same properties as borders, but with the addition of an
outline-offset
prop, and are drawn outside the border/node shape.Much of the implementation mirrors how borders are rendered, but because outlines are drawn outside the border/node, the node shape needs to be redrawn at a slightly larger size than the core, filled shape. For that reason, I opted to move the path caching to a
getPath
method that both the shape rendering anddrawOutline
function can use, rather than duplicating that caching logic.10px, dashed outline:
10px double outline w/ 4px offset:
Triangular node w/ 10px outline:
Rhomboid w/ 10px outline, 8px offset:
TODO:
Checklist
Author:
unstable
. Bug-fix patches can go on eitherunstable
ormaster
.Reviewers:
master
branch and theunstable
branch. Normally, this just requires cherry-picking the corresponding merge commit frommaster
tounstable
-- or vice versa.Closes #3156