Attached is a handy general purpose subnetwork: Morph. Attach a colorized rectangle or list of rectangles to the leftmost "before" port, a different rectangle or set of rectangles to the rightmost "after" port, and then feed a timing element into the center "timing" port. Morph will gradually transform before into after.
The morph node gradually converts a rectangles height, width, position, and fill color. To demonstrate this, the sample network feeds a grid of 36 blue rectangles into the before port and a larger grid of 36 red squares into the after port. As you play or scrub the animation, the blue rectangles explode into a more widely dispersed set of red squares.
The timing takes a percentage value from 0 to 99.99, which you can provide using a "convert range" node. You can then attach the frame node to the convert range node and hit the play button, or scrub the frame counter back and forth, to view the transformation. If you want to separately control multiple transformations you can do this by attaching a simple number node in place of each frame node and then select that number node and scrub its value back and forth between 1 and 100. Each number node becomes, in effect, a separate slider control.
You can also chain multiple transformations together by altering the source start and end values of each convert range node and attaching the frame node to each one. For example, you can set the first transformation to run between frames 1 and 100, the next between frames 101 and 150, the next between frames 151 and 300, etc. The wider the input range, the slower the animation. Hit Play to see the whole thing come together and export it as a movie if you want.
OF course you don't have to change all three properties at once. The morph node can be used just to gradually change colors in stationary rectangles, enlarge or shrink a single rectangle, or simply cause a rectangle to move across the screen. I'm finding it to be a very handy addition to my NodeBox toolkit.
- morph.ndbx 19.2 KB
|?||Show this help|
|ESC||Blurs the current field|
|r||Focus the comment reply box|
|^ + ↩||Submit the comment|
You can use
Command ⌘ instead of
Control ^ on Mac
Support Staff 1 Posted by Frederik De Ble... on 09 Mar, 2015 09:44 AM
Cool! I've attached a screenshot so people can see what it does before downloading.
It seems like it just animates the bounds. Could it work with arbitrary shapes as well (for example, ellipses or text paths)?
Support Staff 2 Posted by john on 10 Mar, 2015 09:24 AM
Frederik, yes, just bounds (height, width, position) plus fill color. And it only works with rectangles. But it's surprising how much you can do just with that.
I would have liked to handle roundness as well, but there doesn't seem to be a way within NodeBox to access that property. If I could, morph would automatically morph from normal rectangles to rounded rectangles. If there was a clean way to determine if one of the incoming shapes was an elipse, morph could substitute a fully-rounded rectangle for the elipse and gradually turn it into a rectangle (or whatever else was attached).
Another key addition would be rotation. It would be handy to feed a before rectangle and an after rotated rectangle; morph would then automatically spin one into the other. But in NodeBox rotation is not a property of a shape; a rotated shape is just a different path.
As you suggest, it would be nicer still if morph could be made even more general purpose. If two arbitrary shapes (paths) have the same number of inflection points you could simply map them directly and transform a huge variety of shapes by simply morphing each line segment the same way I current morph rectangles That would solve the rotation problem. Morphing to or from shapes with a different number of points or with curves would be more complex. You could probably just resample the point count of the more complex "after" shape onto the simpler "before" shape and gradually move those points into the after position keeping them connected. I don't know how you are representing curves, so that would a bit trickier.
You might want to consider adding some kind of morph as an official node. Even my limited version is extremely versatile. All sorts of different complex looking transformations (movement, color cycling, growing, distorting, etc.) can be created effortlessly and intuitively with this single node. By tying the position of other shapes, like text labels, to the output of the morph node you can make compound objects move together. And you have full control over the animation speed by simply varying the range conversion.
Support Staff 3 Posted by john on 10 Mar, 2015 12:51 PM
I couldn't help myself and made a first cut at a generic morph node. I've attached an ndbx file with seven examples and a before/during/after screenshot. To see the transformations hit play or scrub the frames between 1 and 100.
The first example shows the geometrical transformation (you can see the underlying line segments) using the nodes before I grouped them into a subnetwork. The other six examples use the genMorph subnetwork which hides the line segments and also morphs the color; each of those examples goes from blue to red.
The basic approach is to combine the points of shape A with a resample of B's points along shape A. Then do the reverse: combine a resample of A's points along shape B with the points of shape B. Then gradually interpolate between corresponding pairs of points from both lists using Connect to draw intermediate shapes.
The intermediate results are a little spikier than I'd like. The result would be smoother if you could resort the points in both lists to follow along the outline of their shapes in order. Text comes out a bit funky because you can see the connecting lines between individual letters. It doesn't handle any shapes with curves: if you try to morph a rectangle into an ellipse you wind up with an octagon instead.
So this needs work but is a big step closer to the general purpose morph you asked for.
Support Staff 4 Posted by john on 11 Mar, 2015 08:47 AM
Turns out I was overthinking it. Simply pulling the point lists and interpolating (allowing NodeBox to do its wrapping trick if the count is unequal) is simpler and produces cleaner results.
In this version I also added the ability to morph strokeWidths, but ran into a bug. Doing a lookup on strokeWidth works fine UNLESS strokeWidth is zero; in that case the lookup returns a 1. Also, strokeColor is undefined when strokeWidth is zero, so looking it up caused the entire subNetork to fail even if I used a switch. I will file these problems as a bug report so you can track them separately.
Still wish I could handle curves. Any suggestions?
The revised ndbx file shows a few new examples. I was able to make the text less funky by passing it as a list of characters instead of a single string. I have also included an updated set of screenshots. Please disregard my earlier version.
Support Staff 5 Posted by john on 12 Mar, 2015 04:51 AM
It occurred to me that it is strange to post still images of a morph animation when it's so easy to export animations. mp4 attached.
Support Staff 6 Posted by john on 19 Mar, 2015 11:49 AM
OK, so I've been peeking inside of node box SVG files and I think I see a devilishly simple way of making a truly generic morph. Here is what I found:
1. All NodeBox shapes are paths.
2. All NodeBox paths are composed of nothing other than lines and curves.
To clarify, a NodeBox ellipse is not coded as an SVG ellipse object; it is coded as a path consisting of four adjoining curves, where "curve" is a cubic bezier with two control points. NodeBox paths consist of an initial Move (starting location) followed by a series of L point pairs (lines) and C point triplets (curves).
[You can see this by turning on the undocumented "points" feature in the viewer. Apparently inflection points show as blue dots and control points show as red dots.]
First, can the NodeBox staff confirm this? Any exceptions?
If this is true, we could "normalize" all NodeBox shapes by converting every line into a flat curve (a cubic bezier in which the start point, end point, and both control points are collinear). A flat curve would look just like a line segment. Once everything is a series of curves all we have to do is interpolate from each before curve to each after curve.
The morph node would have 3 ports: a before shape, and after shape, and a timing input (a float T from 0 to 100).
- Morph(0) returns the before shape untouched
- Morph(1) returns the normalized before shape (looks the same but has more points in the path)
- Morph(99) returns the normalized after shape
- Morph(100) returns the untouched after shape
Morph (1.01 to 98.99) simply interpolates between each before curve and each after curve. The beginning and ending points gradually move from their before positions to their after positions. Likewise, the two control points gradually move from before to after. If morphing from a line segment to another line segment, the control points should remain collinear at all times so no curves will be seen. If one or both of the before or after segments is a curve, that curve will gradually morph. A line might slowly bend, a concave arc might turn into a convex one, etc.
The interpolation function is trivial. One easy way to do it is just make a line between each pair of before and after points and feed that line (along with the T value) into a point on path node.
If the before shape has more or fewer points than the after shape, NodeBox's wrapping behavior will handle this automatically. So if a triangle morphs into a rectangle, Nodebox will automatically create a fourth edge of the triangle drawn over one of the original edges. All you have to do is feed both normalized lists into an interpolate function and the morphing should just happen naturally.
To create this generic morph node all you need is access to the actual details of the paths (the L's and C's). This is where I hit a wall. I don't know how to do that. By doing a lookup I can derive the points, but can't tell which are inflection points and which are control points. Nor can I create new "normalized" paths consisting entirely of cubic curves.
[Aside: I find it curious that NodeBox shapes are built entirely out of cubic beziers but we are not given a cubic bezier node to make such shapes ourselves, only a quadratic bezier node. I think you can build cubic beziers using pairs of quadratic beziers, but it's a bit of a pain. Why not give us mere mortals the real thing?]
So here is my conundrum. I think Frederik could make an ideal generic morph node in an afternoon and it might instantly become one of the most powerful and versatile nodes in the whole NodeBox arsenal. But even if I tried to roll my own custom node, I don't understand how to access and change the actual coding of NodeBox paths.
Could one of you guys whip up a Morph node and just hand it to us on a silver platter? This could be a fine addition to NodeBox Live as well as NodeBox 3.
If that's not possible, and if there is some way to do what I propose in a custom node, could you give me some clue how to access and modify the hidden structure of NodeBox paths so I can normalize and interpolate them myself?
Support Staff 7 Posted by Frederik De Ble... on 20 Mar, 2015 06:56 PM
Your deduction is correct: all shapes are paths, and all paths consist of béziers and lines.
I've tried in the past to make a generic morph node. Specifically, I wanted it to work on letters. I've gotten quite far but hit a wall, well 2 actually:
1. The starting point of a path is not always the same. So if path A starts at the top and path B starts at the bottom, the morph is going to look weird. I've thought about implementing heuristics that could find the starting point, but I haven't done it yet. In addition,the orientation could be different (clockwise vs counterclockwise); I think that could be detected automatically.
2. Paths can have multiple contours. If we're making a generic morph we can't assume the amount of contours is going to be equal. Even if they are equal, they could be flipped, e.g. The inner contour of the "O" could have index 0 in path A and index 1 in path B.
Anyway, I can send you the code I have so you can take a look at it. If you have any solutions for the two problems above, I'd also love to hear them :-)
Support Staff 8 Posted by john on 20 Mar, 2015 08:32 PM
Yes, I noticed both of those issues but didn't mention them as my post was already pretty long.
I think even a single-contour, starting-point-insensitive morph would be *extremely* useful. It would cover the vast majority of cases outside of letters and even for letters would cover movement, scaling, and rotation, (and color if you add that in as I did).
Even weird morphs are often acceptable, and in many cases they aren't that weird especially when morphing a shape into another version of itself (at different scale, rotation, position, and/or color).
I would dearly love to have such a thing right now. I need to smoothly morph a stacked bar chart into a series of donut charts in specific locations. Your current morph would probably do that in a single bound.
So YES, please post or send me your code! Do you have it in the form of a custom node that I could just snap into place?
In fact, if you don't have time to work on it, I'd encourage you to release it as a built-in node. For v1 just include a note in documentation that it doesn't currently handle multiple contours as gracefully as it might. Later you could issue an update with the improved version; I don't think doing so would break the code of anyone using the more limited morph.
That said, I think both problems are solvable. It sounds like you already have a pretty good idea how to tackle starting position. My first cut at that would have been to define 8 possible compass points for a starting position (North, NorthEast, East, etc.) based on a comparison of the initial path position to the centroid, and then shift the target path until its start matched the same compass point as the source path. You could guess at rotation based on the first two positions in a path with different x values (second point east of start = clockwise).
That approach is not precise, but my hunch is that it most cases it would produce acceptable morphs. I think some mild rotation during a morph is acceptable even when it's not strictly necessary; this technique would probably avoid violent flips and twists. (You may already have an even better approach.)
One important point, though. I think it might be extremely useful to give users an option to control the matching of starting points. Many times they won't care and the default behavior will be good enough. But sometimes they will.
For example, when I morph from a stacked bar to a donut I want all the segments to move together and slowly bend into place. Ideally, if the target donut is west of the bar I want the bar to curve clockwise and if east counterclockwise. Any algorithm you come up with might cause some of the segments to behave differently than their peers or flop about unnecessarily.
One easy way to afford this control would be to simply expose an optional shift index. At 0 (the default) the target path is unshifted relative the normal morphing algorithm. If non-zero, you simply shift the points again after whatever shifting your algorithm already did. The user could experiment with this index until they got the precise affect they wanted. For bonus points you could also provide a second checkbox to reverse the path order of the target path; this would let users switch from clockwise to counterclockwise if need be.
Providing this user control might also relieve any anxiety you have about perfecting your algorithm. Your algorithm doesn't have to be perfect if users have the ability to adjust it when needed. In fact you could release your morph as is (with no attempt to automatically adjust starting position) and let users adjust it themselves (if they care). I would be happy with that.
To handle contours my first instinct is to break the morph into a series of separate simultaneous morphs. To morph from a triangle to the letter O, I would feed the triangle path into the outer contour and simultaneously feed the same triangle path into the inner contour. (This implies an extra step to classify contours as outer vs. inner.)
Morphing multiple-to-multiple contour shapes becomes complex if you to need to determine and respect hierarchies of contours (which in theory could be arbitrarily deep, though in practice more than two levels are probably rare). My instinct here is to not to let the perfect become the enemy of the good. It might be sufficient to map source outer contour to target outer contour and then morph any remaining inner contours to each other at random. For text strings there might be a bit of momentary weirdness inside a subset of letters but I suspect the overall movement would be relatively orderly.
Those are my initial thoughts. If I have any inspirations I will pass them along. In the meantime I look forward to trying your not-yet-perfect-but-alreadly-extremely-useful morphing code.
Support Staff 9 Posted by Frederik De Ble... on 20 Mar, 2015 09:04 PM
The version I had was for NodeBox 2, so I converted it to NodeBox 3. The node is quite simple since it doesn't do any of the heuristics we talked about.
Support Staff 10 Posted by Frederik De Ble... on 20 Mar, 2015 09:05 PM
Here's a screenshot, with the morphing going out of the original bounds to create a "super-fat" letter:
Support Staff 11 Posted by john on 20 Mar, 2015 10:44 PM
This is wonderful! Thank you.
Attached is a sample NodeBox demo I made which morphs a bar (a tall rectangle) into a donut segment (formed by removing an eclipse from an arc). I include both the NodeBox doc and an MP4 showing the transformation.
To begin with I followed your technique of resampling both shapes, but the resulting morph was, as you would say, "weird".
I was able to fix this by shifting and reversing the target path list. I did this by first converting the target path to a list of points (using an earlier subnetwork I made - is there a more elegant way of doing this?). I then fed that list into a shift node, set to 6, and then a reverse node, then a connect and a colorize node to turn it back into a solid shape.
In order to find the magical setting of 6 for the shift I simply advanced the morph to about 20 and then scrubbed along the shift index until the result looked symmetrical. This was not too hard to do, so I think it would be practical to expose a shift parameter and reverse checkbox in your morph node. In fact you could automatically resample as well.
The reverse node was crucial by the way. Without it I was not able to get a non-weird morph.
This morph is not perfect. Ideally, the top and bottom of the bar and the two flat edges of the donut segment should be rendered at all stages as simple line segments. As it is those edges develop a temporary bump during morphing which I'd rather do without. But this is not possible with a general resample along the entire path.
One simple improvement, then, would be to only resample the curves. As I said earlier, I don't know how to see inside the path structure to tell the curves from the lines. Can you do that? If so, it might be fairly simple to only resample the curves.
If you could:
- incorporate curve resampling within the morph function
- wrap unequal path lengths instead of rejecting unequal path lengths
- add a shift integer parameter
- add a reverse boolean parameter
you might have a pretty decent generic morph!
Support Staff 12 Posted by john on 21 Mar, 2015 03:47 AM
One more video example: the word "before" morphing to "after!"
The video shows two versions. The top version just feeds the two words directly into a single instance of your morph node with only one change: I broke each word into a list of individual characters and spaced them equally using a grid.
The bottom version morphs each letter separately (and kerns them together better than a uniform grid allows). For each letter I used the same technique from my bar-to-donut animation to shift the order of points in the target path with each letter getting a different shifting value. None of the letters required reversals.
If you download the video and turn on the looping feature of your video player you can see the difference in each letter. The morphs in the bottom example are a bit smoother.
Adjusting some letters was fairly easy, but others were quite difficult. Some lessons:
1. The contours problem may be overrated. I'm not sure handling them separately would make that much of an improvement (other than getting rid of the barely noticeable cleft between contours).
2. Some morphs are a bit weird no matter what you do. I defy you turn an e into an exclamation mark without some weirdness. I tried countless shift variations but none were noticeably better than the unadjusted version.
3. Shifting made a noticeable improvement in some cases. But overall, I'm not sure the adjusted version is dramatically better than the unadjusted version.
From my previous example I conclude that giving users the option to shift and/or reverse is important.
From this example I conclude that we may be reaching the point of diminishing returns. Fancy start-point optimization and multi-contour handling algorithms would be a ton of work for a fairly modest improvement in most cases. And some morphing weirdness is inescapable.
Support Staff 13 Posted by john on 29 Oct, 2016 02:46 AM
A follow up...
The "bar to donut segment" morph posted above suffered from an unsightly bulge during the transition.
This happens because the outside donut curve is longer than the inside curve. So when the bar morphs it has to make up the difference by borrowing from one side to pay the other.
In order to fix this I broke the rectangle into its four edges and adjusted its sampling so that more points appeared on the right (outer) edge and fewer points on the left.
To figure out the precise number of points needed on each edge of the rectangle, I turned on the "Point Numbers" checkbox, adjusted the points on the donut using shift and reverse nodes so that both the rectangle and donut started in the upper left corner and travelled clockwise, and then noted how many points were on the outer and inner curves of the donut segment.
I have attached the revised networks (in a folder including Frederik's morphing node) and an MP4 showing the improved animation. The pale green bar on the left is the earlier approach with uniform distribution; you can see the bulge appear on the bottom during the transition. The gray bar on the right shows the improved variable distribution. As you can see, the transformation is now smooth and free of bulges.
(There is still a slight imperfection in two corners. That could be corrected by slightly modifying two of the points in the donut distribution, or just using more points altogether.)
The moral of this story is that, even with the current limitations of Frederik's morph node, it is possible to get decent results even for tricky transformations - but only if you are crazy enough to fine tune the distributions.