Filters and Effects
Because Motion Canvas is built on top of the Browser's 2D Rendering Context, wecan make use of several canvas operations that are provided by the Browser.
Filters
Filters let you apply various effects to your nodes. You can find all availablefilters on 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
(
)
)
;
}
)
;
Every node has a
filters
property containing an array of
filters
that will be applied to the node. You candeclare this array yourself, or use the
filters
property to configureindividual filters. Both ways are shown in the following example:
Some filters, like
opacity
and
drop-shadow
, have their own dedicatedproperties directly on the
Node
, class.
import
...
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
}
/>
)
;
// Modification happens by accessing the `filters` property.
// Individual filters don't need to be initialized. If a filter you set doesn't
// exists, it will be automatically created and added to the list of filters.
// If you have multiple filters of the same type, this will only
// modify the first instance (you can use the array method for more control).
yield
*
iconRef
(
)
.
filters
.
blur
(
10
,
1
)
;
yield
*
iconRef
(
)
.
filters
.
blur
(
0
,
1
)
;
}
)
;
Keep in mind that the order in which you apply the effects does matter, as canbe seen in the following example:
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
)
;
}
)
;
Masking and composite operations
Composite operations define how the thing we draw (source) interacts with whatis already on the canvas (destination). Among other things, it allows us todefine complex masks. MDN has a great visualisation of all available composite operations .
You can create a mask by treating one node as the "masking" / "stencil" layer,and another node as the "value" layer. The mask layer will define if the valuelayer will be visible or not. The value layer will be what's actually visible inthe end.
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
)
,
)
;
}
)
;
Any of the following composite operations can be used to create a mask:
source-in
,
source-out
,
destination-in
, and
destination-out
. There isalso a
xor
operation which can be helpful if you want two value layers thathide each other on overlap. Use the dropdown below to browse all examples.
import
...
// Image by Marek Piwnicki (https://unsplash.com/photos/_4o-1pr2oqU)
const
ImageSource
=
'https://images.unsplash.com/photo-1685901088371-f498db7f8c46'
;
export
default
makeScene2D
(
function
*
(
view
)
{
view
.
fill
(
'#141414'
)
;
const
maskRef
=
createRef
<
Img
>
(
)
;
const
valueRef
=
createRef
<
Img
>
(
)
;
yield
view
.
add
(
<
Node
cache
>
{
/** Stencil / Mask Layer. It defines if the Value Layer is visible or not */
}
<
Img
ref
=
{
maskRef
}
size
=
{
250
}
src
=
"
/img/logo_dark.svg
"
/>
{
/** Value Layer. Anything from here will be visible if the Stencil Layer allows for it. */
}
<
Img
ref
=
{
valueRef
}
x
=
{
100
}
src
=
{
ImageSource
}
width
=
{
600
}
compositeOperation
=
{
'source-in'
}
/>
Node
>
,
)
;
yield
maskRef
(
)
.
rotation
(
360
,
4
,
linear
)
;
yield
*
valueRef
(
)
.
x
(
-
100
,
1.5
)
.
wait
(
0.5
)
.
to
(
100
,
1.5
)
.
wait
(
0.5
)
;
}
)
;
Cached nodes
Both filters and composite operations require a cached
Node
. Filters can set it automatically, whilecomposite operations require you to set it explicitly on an ancestor
Node
(usually the parent node).
A cached
Node
and its children are rendered on anoffscreen canvas first, before getting added to the main scene.
For filters this is needed because they are applied to the entire canvas. Bycreating a new canvas and moving the elements that should get affected by thefilters over, applying filters to the entire "new" canvas, and then moving backthe result, you effectively only apply the filters to the moved elements.
To turn a
Node
into a cached node, simply pass the
cache
property
<
Node
cache
>
...
Node
>
// or
<
Node
cache
=
{
true
}
>
...
Node
>
All components inherit from
Node
, so you can setthe cache on all of them.