跳到主要内容

Spline

Click to preview animation

import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';
import {createRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const spline = createRef<Spline>();

view.add(
<Spline ref={spline} lineWidth={4} fill={'#e13238'} closed>
<Knot position={[-120, -30]} startHandle={[0, 70]} />
<Knot
position={[0, -50]}
startHandle={[-40, -60]}
endHandle={[40, -60]}
/>
<Knot position={[120, -30]} startHandle={[0, -70]} />
<Knot position={[0, 100]} startHandle={[5, 0]} />
</Spline>,
);

yield* spline().scale(0.9, 0.6).to(1, 0.4);
});

Spline 组件允许我们通过一系列控制点绘制和动画平滑曲线。

如果您只想绘制一条简单的贝塞尔曲线,请查看贝塞尔曲线组件

定义控制点

为了绘制样条曲线,我们需要指定它的节点是什么。Spline 组件提供了多种指定这些控制点的方法,我们将在本节中介绍这些方法。

使用 points 属性

定义样条曲线节点最简单的方法是通过样条曲线的 points 属性传递位置数组。每个点将被视为样条曲线节点之一的位置。

Click to preview animation

import {makeScene2D, Spline} from '@motion-canvas/2d';

export default makeScene2D(function* (view) {
view.add(
<Spline
lineWidth={6}
stroke={'lightseagreen'}
points={[
[-300, 0],
[-150, -100],
[150, 100],
[300, 0],
]}
/>,
);
});

结果是一条平滑穿过每个提供点的曲线。

记得为样条曲线提供 lineWidthstroke,否则它将不可见。或者,您也可以指定 fill 颜色。

我们可以通过向 smoothness 属性传递 01 之间的值来改变曲线的形状。

Click to preview animation

import {makeScene2D, Spline} from '@motion-canvas/2d';
import {createRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const spline = createRef<Spline>();

view.add(
<Spline
ref={spline}
lineWidth={6}
stroke={'lightseagreen'}
smoothness={0.4}
points={[
[-300, 0],
[-150, -100],
[150, 100],
[300, 0],
]}
/>,
);

yield* spline().smoothness(0, 1).to(1, 1).to(0.4, 1);
});

虽然以这种方式定义节点非常简单,并且对于简单曲线来说已经足够,但这种方法有一个重要的限制:我们无法改变节点控制柄的位置。相反,控制柄会自动计算,以便曲线平滑地穿过每个点,而不会出现任何尖锐或突然的转弯。

有趣的事实

自动控制柄是根据节点的两个相邻节点的位置计算的。以这种方式计算其节点控制柄位置的样条曲线称为基数样条

让我们看看定义节点的第二种方法,以了解我们如何更精细地控制样条曲线的形状。

使用 Knot 节点

定义节点的第二种方法是——恰当地——使用 Knot 节点。上面相同的样条曲线也可以这样写。

Click to preview animation

import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';

export default makeScene2D(function* (view) {
view.add(
<Spline lineWidth={6} stroke={'lightseagreen'}>
<Knot position={[-300, 0]} />
<Knot position={[-150, -100]} />
<Knot position={[150, 100]} />
<Knot position={[300, 0]} />
</Spline>,
);
});

如您所见,我们得到的形状与使用 points 属性时完全相同。使用 Knot 节点定义节点的优点是,它还允许我们通过 startHandleendHandle 属性控制每个节点的控制柄位置。

Click to preview animation

import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';

export default makeScene2D(function* (view) {
view.add(
<Spline lineWidth={6} stroke={'lightseagreen'}>
<Knot position={[-300, 0]} />
<Knot position={[-150, -100]} endHandle={[-100, 0]} />
<Knot position={[150, 100]} startHandle={[100, 0]} />
<Knot position={[300, 0]} />
</Spline>,
);
});

请注意,控制柄位置是相对于节点的位置的。

与使用 points 属性类似,如果没有为节点提供明确的控制柄,控制柄将自动计算,以便曲线平滑地穿过节点。

镜像控制柄

默认情况下,控制柄是镜像的。这意味着当我们只提供一个节点的一个控制柄时,另一个控制柄将隐式设置为所提供控制柄的翻转版本。

<Knot startHandle={[100, 50]} />
// 等同于
<Knot startHandle={[100, 50]} endHandle={[-100, -50]} />

断开的节点

同时提供 startHandleendHandle 会产生所谓的断开节点。断开节点非常有用,因为它们允许我们向样条曲线添加尖角。

Click to preview animation

import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';

export default makeScene2D(function* (view) {
view.add(
<Spline lineWidth={16} stroke={'lightseagreen'} closed>
<Knot position={[-50, -80]} startHandle={[0, 20]} endHandle={[90, 0]} />
<Knot position={[50, 0]} />
<Knot position={[-50, 80]} startHandle={[90, 0]} endHandle={[0, -20]} />
</Spline>,
);
});

在用户控制柄和计算控制柄之间混合

默认情况下,当至少提供 startHandleendHandle 属性之一时,自动计算的控制柄将被忽略。但是,可以通过使用 auto 属性在用户提供的控制柄和自动计算的控制柄之间进行混合。

<Spline lineWidth={16} stroke={'lightseagreen'} closed>
<Knot
position={[-50, -80]}
startHandle={[0, 20]}
endHandle={[90, 0]}
auto={0.5}
/>
<Knot position={[50, 0]} />
<Knot
position={[-50, 80]}
startHandle={[90, 0]}
endHandle={[0, -20]}
auto={0.5}
/>
</Spline>

auto 应该是 01 之间的值,表示在用户提供的控制柄(0)和自动计算的控制柄(1)之间混合的百分比。

auto 是一个复合 signal,这意味着您可以指定 startHandleAutoendHandleAuto 来单独控制每个控制柄的混合。

<Knot
position={[0, 0]}
startHandle={[-50, -50]}
endHandle={[30, 0]}
startHandleAuto={0.3}
endHandleAuto={0.8}
/>

由于 auto 是一个 signal,它也可以动画。

Click to preview animation

import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';
import {all, makeRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const knots: Knot[] = [];

view.add(
<Spline lineWidth={16} stroke={'lightseagreen'} lineJoin={'round'} closed>
<Knot
ref={makeRef(knots, 0)}
position={[-50, -80]}
startHandle={[0, 20]}
endHandle={[90, 0]}
/>
<Knot position={[50, 0]} />
<Knot
ref={makeRef(knots, 1)}
position={[-50, 80]}
startHandle={[90, 0]}
endHandle={[0, -20]}
/>
</Spline>,
);

yield* all(...knots.map(knot => knot.auto(1, 1).to(0, 1)));
});

动画样条曲线

虽然动画样条曲线与动画任何其他节点没有太大区别,但本节旨在说明一些最常见的用例。

绘制样条曲线

Line 组件类似,Spline 节点提供 startend signal,允许我们控制应该可见的曲线段。startend 都是 01 之间的值,表示从样条曲线弧长开始绘制的百分比。

<Spline
points={[
[-300, 0],
[-150, -100],
[150, 100],
]}
start={0.4}
end={0.8}
/>

上面的示例将从样条曲线弧长的 40% 开始绘制样条曲线(start={0.4}),并绘制到样条曲线弧长的 80%(end={0.8})。

当结合使用 startendstartOffsetendOffset 时,startend 将相对于考虑偏移后样条曲线的_剩余_长度。

然后,我们可以通过 tween 这些属性来动画绘制样条曲线:

Click to preview animation

import {makeScene2D, Spline} from '@motion-canvas/2d';
import {all, createRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const spline = createRef<Spline>();

view.add(
<Spline
ref={spline}
lineWidth={6}
stroke={'lightseagreen'}
points={[
[-300, 0],
[-150, -100],
[150, 100],
[300, 0],
]}
end={0}
/>,
);

yield* spline().end(1, 1.5);
yield* spline().start(1, 1.5).to(0.5, 1);
yield* spline().end(0.5, 1);
yield* all(spline().start(0, 1.5), spline().end(1, 1.5));
});

动画样条曲线的节点

Knot 可以像其他组件一样进行动画。

动画样条曲线的节点只有在使用 Knot 组件时才可能,在使用 points 属性时则不行。

以下是通过动画节点的不同属性可以实现的一些有趣效果的示例。

您可以将 startHandleendHandle 视为 Knot 的子节点——改变节点的位置、旋转和缩放也会变换控制柄。唯一的例外是 auto 控制柄,它们不受这些变换的影响。

Click to preview animation

// 片段:位置
import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';
import {all, makeRef, PossibleVector2} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const knotPositions: PossibleVector2[] = [
[-200, 0],
[-100, -80],
[0, 80],
[100, -80],
[200, 0],
];
const knots: Knot[] = [];

view.add(
<Spline lineWidth={6} stroke={'lightseagreen'}>
{knotPositions.map((pos, i) => (
<Knot ref={makeRef(knots, i)} position={pos} />
))}
</Spline>,
);

yield* all(
knots[1].position.y(80, 1).to(-80, 1),
knots[2].position.y(-80, 1).to(80, 1),
knots[3].position.y(80, 1).to(-80, 1),
);
});

// 片段:旋转
import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';
import {createRef, linear} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const knot = createRef<Knot>();

view.add(
<Spline lineWidth={6} stroke={'lightseagreen'}>
<Knot position={[-100, 30]} />
<Knot ref={knot} position={[0, -50]} startHandle={[-70, 0]} />
<Knot position={[100, 30]} />
</Spline>,
);

yield* knot().rotation(360, 3, linear).to(0, 3);
});

// 片段:缩放
import {makeScene2D, Spline, Knot} from '@motion-canvas/2d';
import {createRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const knot = createRef<Knot>();

view.add(
<Spline lineWidth={6} stroke={'lightseagreen'}>
<Knot position={[-100, 30]} />
<Knot ref={knot} position={[0, -50]} startHandle={[-70, 0]} />
<Knot position={[100, 30]} />
</Spline>,
);

yield* knot().scale(3, 2).to(0.2, 2).to(1, 1);
});

沿样条曲线动画对象

样条曲线可用于为对象应遵循的路径建模。您可以使用 getPointAtPercentage 方法来实现这一点。

Click to preview animation

import {makeScene2D, Spline, Rect} from '@motion-canvas/2d';
import {createRef, createSignal} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const spline = createRef<Spline>();
const progress = createSignal(0);

view.add(
<>
<Spline
ref={spline}
lineWidth={6}
stroke={'lightgray'}
points={[
[-300, 0],
[-150, -100],
[150, 100],
[300, 0],
]}
/>
<Rect
size={26}
fill={'lightseagreen'}
position={() => spline().getPointAtPercentage(progress()).position}
rotation={() =>
spline().getPointAtPercentage(progress()).tangent.degrees
}
/>
</>,
);

yield* progress(1, 2).to(0, 2);
});

getPointAtPercentage 方法返回一个 CurvePoint 对象,其中包含位于样条曲线弧长给定百分比的点的位置以及点的切线向量。