滤镜和效果
由于 Motion Canvas 构建在浏览器的 2D 渲染上下文之上,我们可以利用浏览器提供的几个画布操作。
滤镜
滤镜允许您对节点应用各种效果。您可以在 MDN 上找到所有可用的滤镜。
import ...
export default makeScene2D(function* (view) {
view.fill('#141414');
const timePassed = createSignal(0);
const iconRef = createRef<Img>();
const currentEffectText = createSignal('');
yield view.add(
<>
<Img src={'/img/logo_dark.svg'} size={200} x={-200} ref={iconRef} />
<Txt
fill={'rgba(255, 255, 255, 0.6)'}
fontSize={20}
x={200}
text={() => 'Current Filter: ' + currentEffectText()}
/>
</>,
);
function* filters() {
yield currentEffectText('Blur');
yield* iconRef().filters.blur(20, 1);
yield* iconRef().filters.blur(0, 1);
yield currentEffectText('Grayscale');
yield* iconRef().filters.grayscale(1, 1);
yield* iconRef().filters.grayscale(0, 1);
yield currentEffectText('Hue');
yield* iconRef().filters.hue(360, 2);
yield currentEffectText('Contrast');
yield* iconRef().filters.contrast(0, 1);
yield* iconRef().filters.contrast(1, 1);
}
yield* all(timePassed(4, 2 * 4, linear), filters());
});
每个节点都有一个 filters 属性,其中包含将应用于节点的滤镜数组。您可以自己声明此数组,或使用 filters 属性来配置单个滤镜。以下示例显示了两种方式:
一些滤镜(如 opacity 和 drop-shadow)在 Node 类上直接拥有自己专用的属性。
Click to preview animation
// 片段:滤镜属性
import {Img, makeScene2D} from '@motion-canvas/2d';
import {createRef} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
view.fill('#141414');
const iconRef = createRef<Img>();
yield view.add(<Img src={'/img/logo_dark.svg'} size={200} ref={iconRef} />);
// 通过访问 `filters` 属性进行修改。
// 单个滤镜不需要初始化。如果您设置的滤镜不存在,
// 它将自动创建并添加到滤镜列表中。
// 如果您有多个相同类型的滤镜,这只会
// 修改第一个实例(您可以使用数组方法进行更多控制)。
yield* iconRef().filters.blur(10, 1);
yield* iconRef().filters.blur(0, 1);
});
// 片段:滤镜数组
import {makeScene2D, Img, blur} from '@motion-canvas/2d';
import {createSignal} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
view.fill('#141414');
const blurSignal = createSignal(0);
yield view.add(
<Img
src={'/img/logo_dark.svg'}
size={200}
/* 通过更改 'filters' 数组中的滤镜进行修改 */
filters={[blur(blurSignal)]}
/>,
);
yield* blurSignal(10, 1);
yield* blurSignal(0, 1);
});
请记住,应用效果的顺序确实很重要,如以下示例所示:
import ...
export default makeScene2D(function* (view) {
view.fontFamily('monospace').fontSize(20).fill('#141414');
view.add(<Rect size={5000} fill={'#111'} />);
const t = createSignal(0);
const saturateValue = createSignal(1);
const contrastValue = createSignal(1);
view.add(
// Left Segment
<Layout x={-300} direction={'column'} alignItems={'center'} gap={20} layout>
<Circle
size={150}
fill={'#99c47a'}
filters={[saturate(saturateValue), contrast(contrastValue)]}
/>
<Layout direction={'row'} gap={20}>
<Txt fill={'#ffa'}>saturation</Txt>
<Txt fill={'#aff'}>constrast</Txt>
</Layout>
</Layout>,
);
// Right Segment
yield view.add(
<Layout x={300} direction={'column'} alignItems={'center'} gap={20} layout>
<Circle
size={150}
fill={'#99c47a'}
filters={[contrast(contrastValue), saturate(saturateValue)]}
/>
<Layout direction={'row'} gap={20}>
<Txt fill={'#aff'}>constrast</Txt>
<Txt fill={'#ffa'}>saturation</Txt>
</Layout>
</Layout>,
);
// Center Segment
view.add(
<Layout y={-10}>
<Grid size={200} stroke={'gray'} lineWidth={1} spacing={40} />
<Grid size={200} stroke={'#333'} lineWidth={1} spacing={20} />
<Rect size={200} stroke={'gray'} lineWidth={2} />
<Txt
fill={'white'}
text={'saturation'}
rotation={-90}
x={-115}
fill={'#ffa'}
/>
<Txt fill={'white'} text={'contrast'} y={115} fill={'#aff'} />
<Txt fill={'white'} text={'1'} position={[-115, 100]} />
<Txt fill={'white'} text={'1'} position={[-100, 115]} />
<Txt fill={'white'} text={'5'} position={[-115, -90]} />
<Txt fill={'white'} text={'5'} position={[100, 115]} />
<Circle
x={() => map(-150, -100, contrastValue())}
y={() => map(150, 100, saturateValue())}
fill={'white'}
size={20}
/>
</Layout>,
);
yield t(2, 8, linear);
yield* saturateValue(5, 2);
yield* contrastValue(5, 2);
yield* waitFor(1);
yield* saturateValue(1, 2);
yield* contrastValue(1, 2);
});
遮罩和合成操作
合成操作定义我们绘制的内容(源)如何与画布上已有的内容(目标)交互。除其他事项外,它允许我们定义复杂的遮罩。MDN 有一个所有可用合成操作的可视化。
您可以通过将一个节点视为"遮罩"/"模板"层,将另一个节点视为"值"层来创建遮罩。遮罩层将定义值层是否可见。值层将是最终实际可见的内容。
import ...
const ImageSource =
'https://images.unsplash.com/photo-1685901088371-f498db7f8c46';
export default makeScene2D(function* (view) {
view.fontSize(20).fill('#141414');
const valuePosition = createSignal(new Vector2(150, -30));
const maskPosition = createSignal(new Vector2(-150, -30));
const maskLayerRotation = createSignal(0);
const valueLayerRotation = createSignal(0);
const fakeMaskLayerGroup = createRef<Node>();
const fakeValueLayerGroup = createRef<Node>();
// First show fake a Mask Layer. Funnily enough, this also makes use of masking!
yield view.add(
<Node ref={fakeMaskLayerGroup} opacity={0} cache>
<Img
src="/img/logo_dark.svg"
size={200}
position={maskPosition}
rotation={maskLayerRotation}
/>
<Grid
compositeOperation={'source-in'}
stroke={'white'}
width={1000}
height={400}
spacing={5}
lineWidth={1}
/>
</Node>,
);
yield view.add(
<Node ref={fakeValueLayerGroup} opacity={0} cache>
{/*
We do not specifically need to use the Image here, a simple Rectangle would be enough.
It is however convenient because we get the correct aspect ratio.
*/}
<Img
src={ImageSource}
width={360}
position={valuePosition}
rotation={valueLayerRotation}
/>
<Grid
compositeOperation={'source-in'}
stroke={'#ff0'}
width={1000}
rotation={45}
height={1000}
spacing={5}
lineWidth={1}
/>
</Node>,
);
// Legend (Bottom Center)
yield view.add(
<Rect
fill={'#1a1a1aa0'}
layout
direction={'row'}
gap={20}
padding={20}
bottom={() => view.getOriginDelta(Origin.Bottom)}
>
<Layout gap={5} alignItems={'center'}>
<Grid
stroke={'white'}
width={18}
height={18}
spacing={5}
lineWidth={1}
/>
<Txt fill={'white'}>Hidden Stencil / Mask Layer</Txt>
</Layout>
<Layout gap={5} alignItems={'center'}>
<Grid
stroke={'#ff0'}
rotation={45}
width={18}
height={18}
spacing={5}
lineWidth={1}
/>
<Txt fill={'white'}>Hidden Value Layer</Txt>
</Layout>
</Rect>,
);
yield* all(
fakeMaskLayerGroup().opacity(1, 1),
fakeValueLayerGroup().opacity(1, 1),
);
// Here comes the *actual* value and stencil mask. Because it got added last it will be ontop of the "fake" layers.
yield view.add(
<Node cache>
{/** Stencil / Mask Layer. It defines if the Value Layer is visible or not */}
<Img
src="/img/logo_dark.svg"
size={200}
position={maskPosition}
rotation={maskLayerRotation}
/>
{/** Value Layer. Anything from here will be visible if the Stencil Layer allows for it. */}
<Img
src={ImageSource}
width={360}
position={valuePosition}
rotation={valueLayerRotation}
compositeOperation={'source-in'}
/>
</Node>,
);
// Visible Loop
yield* all(
maskPosition(new Vector2(0, -30), 2),
valuePosition(new Vector2(0, -30), 2),
);
yield* maskLayerRotation(360, 2);
yield* valueLayerRotation(-360, 2);
yield* waitFor(1);
yield* all(
maskPosition(new Vector2(-150, -30), 2),
valuePosition(new Vector2(150, -30), 2),
);
yield* all(
fakeMaskLayerGroup().opacity(0, 1),
fakeValueLayerGroup().opacity(0, 1),
);
// Hidden Loop
yield* all(
maskPosition(new Vector2(0, -30), 2),
valuePosition(new Vector2(0, -30), 2),
);
yield* maskLayerRotation(2 * 360, 2);
yield* valueLayerRotation(2 * -360, 2);
yield* waitFor(1);
yield* all(
maskPosition(new Vector2(-150, -30), 2),
valuePosition(new Vector2(150, -30), 2),
);
});
以下任何合成操作都可用于创建遮罩:source-in、source-out、destination-in 和 destination-out。还有一个 xor 操作,如果您想要两个在重叠时相互隐藏的值层,它会有所帮助。使用下面的下拉菜单浏览所有示例。
缓存的节点
滤镜和合成操作都需要一个缓存的 Node。滤镜可以自动设置它,而合成操作需要您在祖先 Node(通常是父节点)上显式设置它。
缓存的 Node 及其子节点首先在离屏画布上渲染,然后再添加到主场景中。对于滤镜,这是必需的,因为它们应用于整个画布。通过创建一个新画布并将应该受滤镜影响的元素移到上面,对整个"新"画布应用滤镜,然后移回结果,您实际上只对移动的元素应用滤镜。
要将 Node 转换为缓存的节点,只需传递 cache 属性
<Node cache>...</Node>
// 或
<Node cache={true}>...</Node>
所有组件都继承自 Node,因此您可以在所有组件上设置缓存。