Animating and displaying pathsΒΆ

The Path node can be used to build animations along complex shapes and can be displayed with the help of RenderPath. It take various PathKey* nodes as input to describe the path itself.

Rendering a path with a single cubic curve looks like this:

A single cubic curve
Code
Graph
import pynopegl as ngl


@ngl.scene()
def simple(cfg: ngl.SceneCfg):
    cfg.aspect_ratio = (1, 1)

    keyframes = [
        ngl.PathKeyMove(to=(-0.7, 0.0, 0.0)),  # starting point
        ngl.PathKeyBezier3(
            control1=(-0.2, -0.9, 0.0),  # first control point
            control2=(0.2, 0.8, 0.0),  # 2nd control point
            to=(0.8, 0.0, 0.0),  # final coordinate
        ),
    ]
    path = ngl.Path(keyframes)
    return ngl.RenderPath(path)
graph

The usage is fairly similar to the interface used in the SVG specifications for building paths. Typically, curves can be chained together to make more complex shapes:

A path composed of a serie of quadratic and cubic curves
Code
Graph
import pynopegl as ngl


@ngl.scene()
def chain(cfg: ngl.SceneCfg):
    cfg.aspect_ratio = (1, 1)

    keyframes = [
        ngl.PathKeyMove(to=(0, 1, 0)),
        ngl.PathKeyBezier3(to=(3, 0, 0), control1=(1, 3, 0), control2=(3, 2, 0)),
        ngl.PathKeyBezier2(to=(0, -3, 0), control=(3, -2, 0)),
        ngl.PathKeyBezier2(to=(-3, 0, 0), control=(-3, -2, 0)),
        ngl.PathKeyBezier3(to=(0, 1, 0), control1=(-3, 2, 0), control2=(-1, 3, 0)),
    ]

    path = ngl.Path(keyframes)
    return ngl.RenderPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), outline_color=(1, 1, 1))
graph

A path can also be composed of several subpaths (by inserting move keys):

A path composed of several sub-paths
Code
Graph
import pynopegl as ngl


@ngl.scene()
def subpaths(cfg: ngl.SceneCfg):
    cfg.aspect_ratio = (1, 1)

    keyframes = [
        ngl.PathKeyMove(to=(0, 1, 0)),
        ngl.PathKeyBezier3(to=(3, 0, 0), control1=(1, 3, 0), control2=(3, 2, 0)),
        ngl.PathKeyBezier2(to=(0, -3, 0), control=(3, -2, 0)),
        ngl.PathKeyBezier2(to=(-3, 0, 0), control=(-3, -2, 0)),
        ngl.PathKeyBezier3(to=(0, 1, 0), control1=(-3, 2, 0), control2=(-1, 3, 0)),
        ngl.PathKeyMove(to=(0, -2, 0)),
        ngl.PathKeyLine(to=(1, -1, 0)),
        ngl.PathKeyBezier3(to=(0, 0, 0), control1=(2, 0, 0), control2=(0.5, 1, 0)),
        ngl.PathKeyBezier3(to=(-1, -1, 0), control1=(-0.5, 1, 0), control2=(-2, 0, 0)),
        ngl.PathKeyClose(),  # close the sub-path of the small heart with a straight line
    ]

    path = ngl.Path(keyframes)
    return ngl.RenderPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), outline_color=(1, 1, 1))
graph

Here the 2nd path is going counter-clockwise (compared to the clockwise outline), so it causes a subtraction.

While a path can be rendered, it can also be used to animate elements:

Animation of an element along a path
Code
Graph
import pynopegl as ngl


@ngl.scene()
def animated(cfg: ngl.SceneCfg):
    cfg.duration = 3
    cfg.aspect_ratio = (1, 1)

    keyframes = [
        ngl.PathKeyMove(to=(0, 1, 0)),
        ngl.PathKeyBezier3(to=(3, 0, 0), control1=(1, 3, 0), control2=(3, 2, 0)),
        ngl.PathKeyBezier2(to=(0, -3, 0), control=(3, -2, 0)),
        ngl.PathKeyBezier2(to=(-3, 0, 0), control=(-3, -2, 0)),
        ngl.PathKeyBezier3(to=(0, 1, 0), control1=(-3, 2, 0), control2=(-1, 3, 0)),
    ]

    path = ngl.Path(keyframes)
    heart = ngl.RenderPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), outline=0.01, outline_color=(1, 1, 1))

    # This animation defines the speed at which the path is walked
    anim_kf = [
        ngl.AnimKeyFrameFloat(0, 0),
        ngl.AnimKeyFrameFloat(cfg.duration, 1, "cubic_in_out"),
    ]

    # We re-use the same shape but we could use anything
    small_heart = ngl.Translate(heart, vector=ngl.AnimatedPath(anim_kf, path))

    # Readjust to fit the viewbox
    small_heart = ngl.Scale(small_heart, (1 / 5, 1 / 5, 1))

    return ngl.Group(children=[heart, small_heart])
graph

Finally, RenderPath has a bunch of effects such as glowing, and like any other render it can be transformed at will:

Glowing beating heart
Code
Graph
import pynopegl as ngl


@ngl.scene()
def effects(cfg: ngl.SceneCfg):
    cfg.aspect_ratio = (1, 1)

    keyframes = [
        ngl.PathKeyMove(to=(0, 1, 0)),
        ngl.PathKeyBezier3(to=(3, 0, 0), control1=(1, 3, 0), control2=(3, 2, 0)),
        ngl.PathKeyBezier2(to=(0, -3, 0), control=(3, -2, 0)),
        ngl.PathKeyBezier2(to=(-3, 0, 0), control=(-3, -2, 0)),
        ngl.PathKeyBezier3(to=(0, 1, 0), control1=(-3, 2, 0), control2=(-1, 3, 0)),
    ]

    path = ngl.Path(keyframes)
    render = ngl.RenderPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), glow=0.02)

    cfg.duration = 0.85
    scale = 1.1
    animkf = [
        ngl.AnimKeyFrameVec3(0, (1, 1, 1)),
        ngl.AnimKeyFrameVec3(0.1, (scale, scale, 1)),
        ngl.AnimKeyFrameVec3(0.2, (1, 1, 1)),
        ngl.AnimKeyFrameVec3(0.3, (scale, scale, 1)),
        ngl.AnimKeyFrameVec3(0.4, (1, 1, 1)),
    ]

    return ngl.Scale(render, factors=ngl.AnimatedVec3(animkf))
graph

The Path node isn’t the only node that can generate a path, SmoothPath is another one where instead of specifying the curves, only points to go through need to be specified (with two extra controls at the extremities).