Animating and displaying pathsΒΆ
The Path node can be used to build animations along complex shapes and can
be displayed with the help of DrawPath. It take various PathKey*
nodes as
input to describe the path itself.
Rendering a path with a single cubic curve looks like this:
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.DrawPath(path)
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:
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.DrawPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), outline_color=(1, 1, 1))
A path can also be composed of several subpaths (by inserting move keys):
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.DrawPath(path, viewbox=(-5, -5, 10, 10), color=(0.8, 0.1, 0.1), outline_color=(1, 1, 1))
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:
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.DrawPath(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])
Finally, DrawPath has a bunch of effects such as glowing, and like any other drawing nodes it can be transformed at will:
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)
draw = ngl.DrawPath(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(draw, factors=ngl.AnimatedVec3(animkf))
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).