Installation

Before you do anything else, make sure to install inkscape, e.g. from https://inkscape.org or using your favourite package manager.

  • Then, clone the camala code from github:
    git clone https://github.com/shimpe/camala
    
  • Second, on the command line create a virtual environment
    cd camala
    python3 -m venv venv
    
  • Third, on the command line activate the virtual environment
    [for windows] venv\Scripts\activate.bat
    [for linux/MacOS] source venv/bin/activate
    
  • Fourth, on the command line install the requirements
    python -m pip install -r requirements.txt
    
  • Fifth, on the command line you can now run the simple ui with
    python src/main.py
    

    Alternatively, you can regenerate all the examples described in the documentation with

    python src/captiongenerator.py
    
  • Sixth, if you want to regenerate the html documentation you will need to install sphinx and run make html in the docs folder. Luckily the documentation is readable online at https://shimpe.github.io/camala
    pip install sphinx
    cd docs
    make html
    
  • Finally, when finished, you can deactivate the virtual environment with
    deactivate
    

Getting Started

Camala (CAption MArkup LAnguage) is a system to generate animated captions. It uses svg, moviepy and inkscape to generate movies from a .toml document with caption generation instructions.

First caption

The Camala language consists of a number of sections with caption generation instruction, some of which are optional. A small camala specification looks as follows:

_images/simple.gif
[Global]
W = "1000"
H = "500"
duration = "1"
fps = "25"
format = "gif"
background = "black"

[Animations]

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Caption.Line1]
pos = "[0, 0]"
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears in the middle of the page."
style = "${Styles.normal}"
  1. there’s a [Global] section. In this section some properties of the generated movie clip are setup. These properties include:

    • W for width [pixels]

    • H for height [pixels]

    • duration [seconds]

    • fps [1/seconds] (frames per second - useful in case animations are present)

    • format [string] (allowed formats: svg, gif, mp4)

    • background [#rrggbb hex color or color name]

  2. there’s an Animations section. This section must always be present, but can be left empty

  3. there’s a Styles.name.StyleProperties section. In the Styles section, we define how captions look. Here you can specify anything you could also specify in CSS (inkscape will do the final interpretation). Typical keys you define here are fill for letter color, stroke for outline color, stroke-width for outline thickness, font-size for font size, font-family for font name, font-style for normal/oblique, and many others.

  4. there’s a Caption section which describes the text that must appear. In this case, the text consists of a single line with position [0, 0] and it uses the normal style defined in the Styles section. In general, captions can consist of multiple lines (each with their own position), and every line can consist of multiple segments, each with their own style.

Changing font properties mid-way

Suppose you want to change the color of the text mid-way the sentence. This can be accomplished by defining an extra style and dividing the line in different segments as shown below:

_images/simple-colorchange.gif
[Global]
W = "1000"
H = "500"
duration = "1"
fps = "25"
format = "gif"
background = "black"

[Animations]

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Styles.special.StyleProperties]
text-anchor="middle"
fill="blue"
stroke="yellow"
stroke-width="0.5px"
font-size="50"
font-family="sans-serif"

[Caption.Line1]
pos = "[0, 200]"   # using a higher y value, moves the text down
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears "
style = "${Styles.normal}"
[Caption.Line1.Segments.Segment2]
text = "in the middle"
style = "${Styles.special}"
[Caption.Line1.Segments.Segment3]
text = " of the page."
style = "${Styles.normal}"

Animating style parameters

Suppose you want the text “in the middle” to change size over time. This can be done by specifying an animation in the Animations section, and referring to it from the Styles section.

_images/simple-animatedstyle.gif
[Global]
W = "1000"
H = "500"
duration = "3"
fps = "25"
format = "gif"
background = "black"

[Animations.Style.grow]
type = "NumberAnimation"
begin = "0"
end = "50"
tween = "easeOutBounce"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Styles.special.StyleProperties]
text-anchor="middle"
fill="blue"
stroke="yellow"
stroke-width="0.5px"
font-size="${Animations.Style.grow}" # refers to Animations.Style.grow
font-family="sans-serif"
[Styles.special.StyleAnimation.grow]
birth_time = '0' # birth_time specifies when the animation comes into existence
begin_time = '0' # begin_time says when the animation starts animating
                 # before the begin_time, the animation just returns the initial value
end_time = '2*${Global.duration}/3' # end_time says when the animation has to be fully completed
death_time = '${Global.duration}' # between end_time and death_time the animation returns the last value

[Caption.Line1]
pos = "[0, 200]"   # using a higher y value, moves the text down
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears "
style = "${Styles.normal}"
[Caption.Line1.Segments.Segment2]
text = "in the middle"
style = "${Styles.special}"
[Caption.Line1.Segments.Segment3]
text = " of the page."
style = "${Styles.normal}"

Notice how the font-size now has a value of "${Animations.Style.grow}", and the Animations.Style.grow section describes a NumberAnimation from 0 to 50 with tweening. In the Style definition where the animation is used, there’s now also a StyleAnimation section added, which says when the animation starts animating, and when it stops animating.

Animating multiple style parameters

You can animate multiple style parameters independently. E.g. here I also change the letter-spacing of the first line segment.

_images/simple-animatedstyle2.gif
[Global]
W = "1000"
H = "500"
duration = "3"
fps = "25"
format = "gif"
background = "black"

[Animations.Style.grow]
type = "NumberAnimation"
begin = "0"
end = "50"
tween = "easeOutBounce"
[Animations.Style.shrink]
type = "NumberAnimation"
begin = "10"
end = "0"
tween = "linear"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Styles.special.StyleProperties]
text-anchor="middle"
fill="blue"
stroke="yellow"
stroke-width="0.5px"
font-size="${Animations.Style.grow}" # refers to Animations.Style.grow
font-family="sans-serif"
[Styles.special.StyleAnimation.grow]
birth_time = '0' # birth_time specifies when the animation comes into existence
begin_time = '0' # begin_time says when the animation starts animating
                 # before the begin_time, the animation just returns the initial value
end_time = '2*${Global.duration}/3' # end_time says when the animation has to be fully completed
death_time = '${Global.duration}' # between end_time and death_time the animation returns the last value

[Styles.firstseg.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"
letter-spacing="${Animations.Style.shrink}"
[Styles.firstseg.StyleAnimation.shrink]
birth_time = '0' # birth_time specifies when the animation comes into existence
begin_time = '${Global.duration}/3' # begin_time says when the animation starts animating
                 # before the begin_time, the animation just returns the initial value
end_time = '${Global.duration}' # end_time says when the animation has to be fully completed
death_time = '${Global.duration}' # between end_time and death_time the animation returns the last value

[Caption.Line1]
pos = "[0, 200]"   # using a higher y value, moves the text down
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears "
style = "${Styles.firstseg}"
[Caption.Line1.Segments.Segment2]
text = "in the middle"
style = "${Styles.special}"
[Caption.Line1.Segments.Segment3]
text = " of the page."
style = "${Styles.normal}"

Sequencing style animations

You can sequence different animations one after the other into a new animation by using SequentialAnimation instead of NumberAnimation. E.g. here’s an example of sequencing a grow and shrink animation to get a pulsing animation.

_images/sequential-style-animation.gif
[Global]
W = "1000"
H = "500"
duration = "3"
fps = "25"
format = "gif"
background = "black"

[Animations.Style.grow]
type = "NumberAnimation"
begin = "0"
end = "50"
tween = "easeOutBounce"
[Animations.Style.shrink]
type = "NumberAnimation"
begin = "50"
end = "0"
tween = "easeOutQuad"
[Animations.Style.pulse]
type = "SequentialAnimation"
elements = "[${Animations.Style.grow}, ${Animations.Style.shrink}]"
time_weights = "[1.0, 0.75]"
repeats = "2"
tween = "linear"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Styles.special.StyleProperties]
text-anchor="middle"
fill="blue"
stroke="yellow"
stroke-width="0.5px"
font-size="${Animations.Style.pulse}" # refers to Animations.Style.pulse
font-family="sans-serif"
[Styles.special.StyleAnimation.pulse]
birth_time = '0' # birth_time specifies when the animation comes into existence
begin_time = '0' # begin_time says when the animation starts animating
                 # before the begin_time, the animation just returns the initial value
end_time = '7*${Global.duration}/8' # end_time says when the animation has to be fully completed
death_time = '${Global.duration}' # between end_time and death_time the animation returns the last value

[Caption.Line1]
pos = "[0, 200]"   # using a higher y value, moves the text down
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears "
style = "${Styles.normal}"
[Caption.Line1.Segments.Segment2]
text = "in the middle"
style = "${Styles.special}"
[Caption.Line1.Segments.Segment3]
text = " of the page."
style = "${Styles.normal}"

Notice how the SequentialAnimation takes different parameters from a NumberAnimation. SequentialAnimation takes a list of animations and will perform these one after the other.

  • elements contains the list of animations to sequence

  • time_weights describes how much of the total animation time should be spent in each of the child animations

  • repeats says how often the sequence must be repeated

  • tween allows to specify an extra tweening on top of the already present tweening in the child animations

Animating the position

In addition to animating the appearance of the text it’s also possible to animate the position of the text. This is accomplished by adding a Position section in the Animations and referring to it from the Caption.line pos field.

_images/position-animation.gif
[Global]
W = "1000"
H = "500"
duration = "1"
fps = "25"
format = "gif"
background = "black"

[Animations.Position.top_to_bottom]
type = "PointAnimation"
begin = "[0, -${Global.H}/3]"
end = "[0, ${Global.H}/3]"
tween = "linear"
ytween = "easeOutBounce"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Caption.Line1]
pos = "${Animations.Position.top_to_bottom}"
[Caption.Line1.PositionAnimation]
birth_time = "0"
begin_time = "${Global.duration}/4"
end_time = "${Global.duration}*2/3"
death_time = "${Global.duration}"
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears in the middle of the page."
style = "${Styles.normal}"

Summing animations

If you want to lower two lines from the top of the screen to the bottom of the screen, maybe you want to reuse the same top_to_bottom animation, but specify a constant offset between the positions of the first and second line. This is possible with a SumAnimation.

_images/position-sumanimation.gif
[Global]
W = "1000"
H = "500"
duration = "1"
fps = "25"
format = "gif"
background = "black"

[Animations.Position.top_to_bottom]
type = "PointAnimation"
begin = "[0, -${Global.H}/3]"
end = "[0, ${Global.H}/3]"
tween = "linear"
ytween = "easeOutBounce"
[Animations.Position.constant_offset]
type = "PointAnimation"
begin = "[0, 40]"
end = "[0, 40]"
tween = "linear"
ytween = "linear"
[Animations.Position.top_to_bottom_shifted]
type = "SumAnimation"
elements = "[${Animations.Position.top_to_bottom}, ${Animations.Position.constant_offset}]"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Caption.Line1]
pos = "${Animations.Position.top_to_bottom}"
[Caption.Line1.PositionAnimation]
birth_time = "0"
begin_time = "${Global.duration}/4"
end_time = "${Global.duration}*2/3"
death_time = "${Global.duration}"
[Caption.Line1.Segments.Segment1]
text= "Simple caption that appears in the middle of the page."
style = "${Styles.normal}"
[Caption.Line2]
pos = "${Animations.Position.top_to_bottom_shifted}"
[Caption.Line2.PositionAnimation]
birth_time = "0"
begin_time = "${Global.duration}/4"
end_time = "${Global.duration}*2/3"
death_time = "${Global.duration}"
[Caption.Line2.Segments.Segment1]
text= "Second line with constant offset to first line."
style = "${Styles.normal}"

Of course you can also sum more complex animations.

TextProvider and TextProviderAnimation

Sometimes you may want text to appear or disappear character by character (e.g. like in a typewriter effect). In Camala this is possible by creating an Animations.TextProvider.name animation. Such Animation must generate numbers between 0 and 100 where 0 means that a single character is shown, and 100 means 100% of all characers are shown. You can also generate negative numbers up to -100. If the animation produces a negative value V than the last V pct of characters are shown instead of the first V pct of characters. Here’s an example:

_images/textprovider.gif
[Global]
W = "1000"
H = "500"
duration = "10"
fps = "25"
format = "gif"
background = "black"

[Animations]
    [Animations.TextProvider.appear]
        type = "NumberAnimation"
        begin = "0"
        end = "100"
        tween = "easeOutQuad"
    [Animations.TextProvider.disappear]
        type = "NumberAnimation"
        begin = "100"
        end = "0"
        tween = "easeOutQuad"
    [Animations.TextProvider.reverse_appear]
        type = "NumberAnimation"
        begin = "0"
        end = "-100"
        tween = "easeOutQuad"
    [Animations.TextProvider.flash]
        type = "SequentialAnimation"
        elements = "[${Animations.TextProvider.appear}, ${Animations.TextProvider.disappear}]"
        time_weights = "[1,1]"
        repeats = "3"
        tween = "linear"
    [Animations.TextProvider.flash_and_appear]
        type = "SequentialAnimation"
        elements = "[${Animations.TextProvider.flash}, ${Animations.TextProvider.appear}]"
        time_weights = "[1,3]"
        repeats = "1"
        tween = "linear"
[Styles]
    [Styles.normal.StyleProperties]
        text-anchor="middle"
        fill="white"
        stroke="black"
        stroke-width="2px"
        font-size="55"
        font-family="sans-serif"
    [Styles.special.StyleProperties]
        text-anchor="middle"
        fill="blue"
        stroke="yellow"
        stroke-width="1px"
        font-size="70"
        font-family="sans-serif"
[Caption]
    [Caption.Line1]
        pos = "[0, 0]"
        [Caption.Line1.TextProvider]
            style = "${Animations.TextProvider.reverse_appear}"
        [Caption.Line1.TextProviderAnimation]
            birth_time = "0"
            begin_time = "0"
            end_time = "${Global.duration}/2"
            death_time = "${Global.duration}"
        [Caption.Line1.Segments]
            [Caption.Line1.Segments.Segment1]
                text= "Stuck in "
                style = "${Styles.normal}"
            [Caption.Line1.Segments.Segment2]
                style = "${Styles.special}"
                text = "the middle"
            [Caption.Line1.Segments.Segment3]
                style = "${Styles.normal}"
                text = " with you!"

Playing with CaptionSvgAttribute and SegmentSvgAttribute

The animated captions are made by generating an svg for every frame, and rendering it to png with inkscape (and later to .gif or .mp4 with moviepy). If desired you can directly inject some attributes into the <text> and <tspan> elements. A Caption is translated into <text>; a segment is translated into <tspan>. Some attributes cannot be specified in a style (or rather: you can specify them but they don’t work as expected). One such example is the “rotate” attribute. If you want to rotate characters with the rotate attribute, you need to specify it with a CaptionSvgAttribute or a SegmentSvgAttribute. CaptionSvgAttribute and SegmentSvgAttribte can be animated. See here a complex example combining many of the techniques mentioned before:

_images/complex.gif
[Global]
W = "1000"
H = "500"
duration = "10"
fps = "25"
format = "gif"
background = "#ea7ce0"

[Animations]
    [Animations.TextProvider]
        [Animations.TextProvider.show]
            type = "NumberAnimation"
            begin = "100"
            end = "100"
            tween = "linear"
        [Animations.TextProvider.typewriter]
            type = "NumberAnimation"
            begin = "0"
            end = "100"
            tween = "easeInQuad"
        [Animations.TextProvider.typewriter2]
            type = "NumberAnimation"
            begin = "0"
            end = "-100"
            tween = "easeInQuad"
    [Animations.Position]
        [Animations.Position.top_to_bottom]
            type = "PointAnimation"
            begin = "[0, -${Global.H}/1.5]"
            end = "[0, ${Global.H}/3]"
            tween = "easeOutBounce"
            ytween = "easeOutBounce"
        [Animations.Position.right_to_left]
            type = "PointAnimation"
            begin = "[${Global.W}/5, -${Global.H}/3]"
            end =   "[-${Global.W}/5, -${Global.H}/3]"
            tween = "linear"
            ytween = "linear"
        [Animations.Position.left_to_right]
            type = "PointAnimation"
            begin = "[-${Global.W}/5, -${Global.H}/3]"
            end =   "[ ${Global.W}/5, -${Global.H}/3]"
            tween = "linear"
            ytween = "linear"
        [Animations.Position.multi]
            type = "SequentialAnimation"
            elements = "[${Animations.Position.left_to_right}, ${Animations.Position.right_to_left}]"
            time_weights = "[1.0, 0.75]"
            repeats = "3"
            tween = "easeOutQuad"
        [Animations.Position.offset]
            type = "PointAnimation"
            begin = "[10, 30]"
            end = "[10, 30]"
            tween = "linear"
            ytween = "linear"
        [Animations.Position.top_to_bottom_with_offset]
            type = "SumAnimation"
            elements = "[${Animations.Position.top_to_bottom}, ${Animations.Position.offset}]"

    [Animations.Style]
        [Animations.Style.grow]
            type = "NumberAnimation"
            begin = "0"
            end = "100"
            tween = "easeOutBounce"
        [Animations.Style.shrink]
            type = "NumberAnimation"
            begin = "100"
            end = "0"
            tween = "easeOutBounce"
        [Animations.Style.pulse]
            type = "SequentialAnimation"
            elements = "[${Animations.Style.grow}, ${Animations.Style.shrink}]"
            time_weights = "[1.0, 1.0]"
            repeats = "3"
        [Animations.Style.space_shrink]
            type = "NumberAnimation"
            begin = "50"
            end = "0"
            tween = "linear"
        [Animations.Style.space_grow]
            type = "NumberAnimation"
            begin = "-50"
            end = "0"
            tween = "linear"
    [Animations.CaptionSvgAttribute]
        [Animations.CaptionSvgAttribute.rotate]
            type = "NumberAnimation"
            begin = "-30"
            end = "30"
            tween = "easeOutBounce"
        [Animations.CaptionSvgAttribute.rotate.CaptionSvgAttributeAnimation]
            birth_time = "0"
            begin_time = "0"
            end_time = "${Global.duration}/2"
            death_time = "${Global.duration}"
    [Animations.SegmentSvgAttribute]
        [Animations.SegmentSvgAttribute.rotate]
            type = "NumberAnimation"
            begin = "45"
            end = "-45"
            tween = "easeOutBounce"
        [Animations.SegmentSvgAttribute.rotate.SegmentSvgAttributeAnimation]
            birth_time = "0"
            begin_time = "${Global.duration}/2"
            end_time = "${Global.duration}"
            death_time = "${Global.duration}"
[Styles]
    [Styles.normal]
        [Styles.normal.StyleProperties]
            text-anchor="middle"
            fill="white"
            stroke="black"
            stroke-width="2px"
            font-size="55"
            font-family="sans-serif"
    [Styles.h1]
        [Styles.h1.StyleProperties]
            text-anchor="middle"
            stroke-width="2px"
            font-family="serif"
            fill="blue"
            stroke="gold"
            font-size="80"
    [Styles.h2]
        [Styles.h2.StyleProperties]
            text-anchor="middle"
            stroke-width="2px"
            font-family="sans-serif"
            fill="gold"
            stroke="maroon"
            font-size="33"
            font-style="italic"
    [Styles.h3_grow]
        [Styles.h3_grow.StyleProperties]
            text-anchor="middle"
            font-family="serif"
            fill="green"
            stroke="white"
            stroke-width="3px"
            font-size="${Animations.Style.grow}"
        [Styles.h3_grow.StyleAnimation]
            [Styles.h3_grow.StyleAnimation.grow]
                begin_time = '${Global.duration}/3'
                end_time = '2*${Global.duration}/3'
    [Styles.h3_pulse]
        [Styles.h3_pulse.StyleProperties]
            text-anchor="middle"
            font-family="serif"
            fill="green"
            stroke="white"
            stroke-width="3px"
            font-size="${Animations.Style.grow}"
            letter-spacing="${Animations.Style.space_shrink}"
            word-spacing="${Animations.Style.space_grow}"
        [Styles.h3_pulse.StyleAnimation]
            [Styles.h3_pulse.StyleAnimation.grow]
                begin_time = '0'
                end_time = '${Global.duration}'
            [Styles.h3_pulse.StyleAnimation.pulse]
                begin_time = '0'
                end_time = '${Global.duration}'
            [Styles.h3_pulse.StyleAnimation.space_shrink]
                begin_time = '0'
                end_time = '${Global.duration}'

[Caption]
    [Caption.Line1]
        pos = "[0, 0]"
        [Caption.Line1.CaptionSvgAttribute]
            rotate = "${Animations.CaptionSvgAttribute.rotate}"
        [Caption.Line1.TextProvider]
            style = "${Animations.TextProvider.show}"
        [Caption.Line1.Segments]
            [Caption.Line1.Segments.Segment1]
                text= "Stuck in "
                style = "${Styles.normal}"
            [Caption.Line1.Segments.Segment2]
                style = "${Styles.h3_grow}"
                text = "the middle"
            [Caption.Line1.Segments.Segment2.SegmentSvgAttribute]
                rotate = "${Animations.SegmentSvgAttribute.rotate}"
            [Caption.Line1.Segments.Segment3]
                style = "${Styles.normal}"
                text = " with you!"
    [Caption.Line2]
        pos = "${Animations.Position.top_to_bottom}"
        [Caption.Line2.PositionAnimation]
            birth_time = "0"
            begin_time = "${Global.duration}/4"
            end_time = "${Global.duration}*2/3"
            death_time = "${Global.duration}"
        [Caption.Line2.TextProvider]
            style = "${Animations.TextProvider.typewriter}"
        [Caption.Line2.TextProviderAnimation]
            birth_time = "0"
            begin_time = "${Global.duration}/4"
            end_time = "${Global.duration}*2/3"
            death_time = "${Global.duration}"
        [Caption.Line2.Segments]
            [Caption.Line2.Segments.Segment1]
                style = "${Styles.h3_pulse}"
                text= "London Bridge "
            [Caption.Line2.Segments.Segment2]
                style = "${Styles.h2}"
                text = "is falling down!"
                dy = "1.5em"
    [Caption.Line3]
        pos = "${Animations.Position.top_to_bottom_with_offset}"
        [Caption.Line3.PositionAnimation]
            birth_time = "${Global.duration}/2"
            begin_time = "${Global.duration}/2"
            end_time = "${Global.duration}"
            death_time = "${Global.duration}"
        [Caption.Line3.TextProvider]
            style = "${Animations.TextProvider.typewriter2}"
        [Caption.Line3.TextProviderAnimation]
            birth_time = "0"
            begin_time = "${Global.duration}*0.5"
            end_time = "${Global.duration}*0.8"
            death_time = "${Global.duration}"
        [Caption.Line3.Segments]
            [Caption.Line3.Segments.Segment1]
                style = "${Styles.h3_pulse}"
                text= "London Bridge "
            [Caption.Line3.Segments.Segment2]
                style = "${Styles.h2}"
                text = "is falling down!"
                dy = "1.5em"

Text along a path

_images/textpath.gif

Sometimes you may want text to follow a curved path instead of a straight line. SVG has provisions for doing exactly that. The way you can tap into these possibilities with camala is as follows:

First you need to define a path in the (optional) Paths section. The easiest way to come up with a path definition, is to create one in a program like inkscape. Then copy the path definition from inkscape’s XML editor. If you work with objects in inkscape, be sure to convert them to path. If you have multiple paths in inkscape, be sure to merge them together into a single path (see separate section on defining a path with inkscape for some tips and tricks).

[Paths.mypath]
d = "M -78.852544,-240.20076 C 58.855926,-262.32979 196.64283,-169.19795 218.39473,-29.553448 237.98768,96.230928 152.64092,221.95741 24.908313,241.1001 -88.950691,258.16363 -202.62691,180.59621 -219.15153,64.774609 -233.69446,-37.157379 -163.89861,-138.7972 -59.986937,-152.69151 30.015762,-164.72602 119.63885,-102.69091 130.88547,-10.687839 140.42892,67.382399 86.138653,145.01798 6.042703,153.59086 -60.090315,160.66925 -125.78459,114.09913 -131.64228,45.909002 -136.29716,-8.2790466 -97.40527,-62.111562 -41.121326,-65.182267 1.1071754,-67.486139 43.232919,-36.191314 43.376226,8.1777716 43.473873,38.41142 19.577331,69.196211 -12.822905,66.081605 -30.936089,64.340402 -51.542578,47.057084 -44.133023,27.043382 c 2.734212,-7.38529 19.039637,-17.8791264 21.877312,-4.716401"

Next, in a Caption.line you need to reference the path instead of the position:

[Caption.Line1]
path = "${Paths.mypath}"

You can set up path properties in the PathProperties section, e.g.

[Caption.Line1.PathProperties]
lengthAdjust = "spacing" # or spacingAndGlyphs
method = "align" # or stretch
side = "left" # or right
spacing = "auto" # or auto
startOffset = "${Animations.Path.increase}"
#textLength = "135" #a number

Note that the startOffset is animated. The animation is defined in the Animations.Path.animationname section:

[Animations.Path.increase]
type = "NumberAnimation"
begin = "0"
end = "2253"
tween = "easeOutBounce"

And the birth_time, begin_time, end_time and death_time for the animation are defined in the Caption.line.PathAnimation section:

[Caption.Line1.PathAnimation.increase]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}-2"
death_time = "${Global.duration}"

The complete spec for the animated text therefore becomes:

[Global]
W = "1000"
H = "500"
duration = "15"
fps = "25"
format = "gif"
background = "black"

[Paths.mypath]
# design the path with e.g. inkscape
d = "M -78.852544,-240.20076 C 58.855926,-262.32979 196.64283,-169.19795 218.39473,-29.553448 237.98768,96.230928 152.64092,221.95741 24.908313,241.1001 -88.950691,258.16363 -202.62691,180.59621 -219.15153,64.774609 -233.69446,-37.157379 -163.89861,-138.7972 -59.986937,-152.69151 30.015762,-164.72602 119.63885,-102.69091 130.88547,-10.687839 140.42892,67.382399 86.138653,145.01798 6.042703,153.59086 -60.090315,160.66925 -125.78459,114.09913 -131.64228,45.909002 -136.29716,-8.2790466 -97.40527,-62.111562 -41.121326,-65.182267 1.1071754,-67.486139 43.232919,-36.191314 43.376226,8.1777716 43.473873,38.41142 19.577331,69.196211 -12.822905,66.081605 -30.936089,64.340402 -51.542578,47.057084 -44.133023,27.043382 c 2.734212,-7.38529 19.039637,-17.8791264 21.877312,-4.716401"

[Animations.Path.increase]
type = "NumberAnimation"
begin = "0"
end = "2253"
tween = "easeOutBounce"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="25"
font-family="sans-serif"

[Caption.Line1]
path = "${Paths.mypath}"
[Caption.Line1.PathProperties]
lengthAdjust = "spacing" # or spacingAndGlyphs
method = "align" # or stretch
side = "left" # or right
spacing = "auto" # or auto
startOffset = "${Animations.Path.increase}"
#textLength = "135" #a number
[Caption.Line1.PathAnimation.increase]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}-2"
death_time = "${Global.duration}"

[Caption.Line1.Segments.Segment1]
text= "Simple caption that follows a path."
style = "${Styles.normal}"

Modifying appearance with SVG filters

_images/textfilters2.gif

Introduction

The look and feel of text can be drastically altered by applying SVG filters. SVG filters are supported in camala in the form of filter plugins. Here are some examples of using SVG filters to introduce distortion and blurring:

_images/textfilter.gif
[Global]
W = "1000"
H = "500"
duration = "5"
fps = "25"
format = "gif"
background = "black"

[Animations.Filter.seedchange]
type = "NumberAnimation"
begin = "0"
end = "${Global.fps}*${Global.duration}/2.5"
tween = "linear"
[Animations.Filter.seedchange.FilterAnimation.seed]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}"
death_time = "${Global.duration}"
[Animations.Filter.scalereduction]
type = "NumberAnimation"
begin = "25"
end = "0"
tween = "linear"
[Animations.Filter.scalereduction.FilterAnimation.scale]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}"
death_time = "${Global.duration}"
[Animations.Filter.sharpen]
type = "NumberAnimation"
begin = "60"
end = "0"
tween = "easeOutQuad"
[Animations.Filter.sharpen.FilterAnimation.stdDeviationx]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}*0.8"
death_time = "${Global.duration}"
[Animations.Filter.sharpen.FilterAnimation.stdDeviationy]
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dxred]
type="NumberAnimation"
begin="5"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dxred.FilterAnimation.dx_red]
birth_time = "0"
begin_time = "1.3"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dyred]
type="NumberAnimation"
begin="-6"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dyred.FilterAnimation.dy_red]
birth_time = "0"
begin_time = "0.6"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dxgreen]
type="NumberAnimation"
begin="4"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dxgreen.FilterAnimation.dx_green]
birth_time = "0"
begin_time = "0.5"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dygreen]
type="NumberAnimation"
begin="-3"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dygreen.FilterAnimation.dy_green]
birth_time = "0"
begin_time = "0.6"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dxblue]
type="NumberAnimation"
begin="7"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dxblue.FilterAnimation.dx_blue]
birth_time = "0"
begin_time = "0.7"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"
[Animations.Filter.dyblue]
type="NumberAnimation"
begin="-5"
end = "0"
tween="easeOutBounce"
[Animations.Filter.dyblue.FilterAnimation.dy_blue]
birth_time = "0"
begin_time = "1"
end_time = "${Global.duration}*0.5"
death_time = "${Global.duration}"

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="100"
font-family="sans-serif"
[Styles.small.StyleProperties]
text-anchor="middle"
fill="white"
stroke="black"
stroke-width="0.5px"
font-size="50"
font-family="sans-serif"

[Caption.Line1]
pos = "[0, 0]"
[Caption.Line1.Filter]
filter = "${Filters.displacement}" # points to displacement.svgtemplate in the templates/filter folder
[Caption.Line1.Filter.Overrides]
numOctaves = "1" # overrides default values specified in displacment.toml in the templates/filter folder
scale = "${Animations.Filter.scalereduction}"
seed = "${Animations.Filter.seedchange}"
[Caption.Line1.Segments.Segment1]
text= "Distortion"
style = "${Styles.normal}"
[Caption.Line2]
pos = "[0, 75]"
[Caption.Line2.Filter]
filter = "${Filters.blur}"
[Caption.Line2.Filter.Overrides]
stdDeviationx = "${Animations.Filter.sharpen}"
stdDeviationy = "${Animations.Filter.sharpen}"
[Caption.Line2.Segments.Segment1]
text = "Blur blur blur"
style = "${Styles.normal}"
[Caption.Line3]
pos = "[0, 175]"
[Caption.Line3.Filter]
filter = "${Filters.rgbsplit}"
[Caption.Line3.Filter.Overrides]
dx_red = "${Animations.Filter.dxred}"
dy_red = "${Animations.Filter.dyred}"
dx_green = "${Animations.Filter.dxgreen}"
dy_green = "${Animations.Filter.dygreen}"
dx_blue = "${Animations.Filter.dxblue}"
dy_blue = "${Animations.Filter.dyblue}"
[Caption.Line3.Segments.Segment1]
text = "RGB split?"
style= "${Styles.small}"
[Caption.Line4]
pos = "[0, -75]"
[Caption.Line4.Filter]
filter = "${Filters.dropshadow}"
[Caption.Line4.Filter.Overrides]
floodcolor = "#8f8b19"
[Caption.Line4.Segments.Segment1]
text = "drop it!"
style= "${Styles.small}"
[Caption.Line5]
pos = "[50, -175]"
[Caption.Line5.Filter]
filter = "${Filters.sketchy}"
[Caption.Line5.Filter.Overrides]
floodcolor = "#8f8b19"
[Caption.Line5.Segments.Segment1]
text = "sketchy?!"
style= "${Styles.normal}"

Using an SVG filter plugin

An SVG filter plugin consists of two files located in the templates/filters subfolder. Suppose you want to add a blur effect onto your text. In the templates/filters subfolder there’s a blur.svgtemplate and a blur.toml file. In the blur.toml file, you find a list of all parameters the plugin takes, and their default values. If you don’t override their value in your camala specification, those default values will be used instead.

To use a filter plugin in camala, you need to reference it in a line:
[Caption.Line1.Filter]
filter = "${Filters.displacement}"
By writing the above, you point to a plugin consisting of files displacement.svgtemplate and displacement.toml in the templates/filters subfolder. To set the filter parameters other than the default values, add an Overrides section:
[Caption.Line1.Filter.Overrides]
scale = "2"
As with other parameters, the filter parameters can be animated. To animate filter parameters, define an animation in the Animations.Filter section, and reference it in the Overrides section.
[Animations.Filter.scalereduce]
type = "NumberAnimation"
begin = "10"
end = "0"
tween = "linear"
#...
[Caption.Line1.Filter]
filter = "${Filters.displacement}"
[Caption.Line1.Filter.Overrides]
scale = "${Animations.Filter.scalereduce}"

Similar to how it’s done for style values, birth_time, begin_time, end_time and death_time are specified in the Animations.Filter.scalereduce.FilterAnimation.scale section.

[Animations.Filter.scalereduce]
type = "NumberAnimation"
begin = "10"
end = "0"
tween = "linear"
[Animations.Filter.scalereduce.FilterAnimation.scale] # animation times for scale parameter animated with scalereduce animation
birth_time = "0"
begin_time = "0"
end_time = "${Global.duration}*0.75"
death_time = "${Global.duration}"
#...
[Caption.Line1.Filter]
filter = "${Filters.displacement}"
[Caption.Line1.Filter.Overrides]
scale = "${Animations.Filter.scalereduce}"

Adding your own SVG filters

Since the plugins consist of just two files, nothing stops you from adding SVG filters of your own. Some rules have to be observed. Consider the blur plugin:

<filter id="blur_${line}" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="${Animations.Filter.colorInterpolationFilters_${line}}">
	<feGaussianBlur stdDeviation="${Animations.Filter.stdDeviationx_${line}} ${Animations.Filter.stdDeviationy_${line}}" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"/>
</filter>

The filter id must have the same name as the file without extension, and augmented with _${line}. Each parameter in the svg template that you want to expose to the user, must be named using a specific naming scheme. Suppose you want to expose parameters stDeviationx and stdDeviationy for the blurring filter, then in the svg attribute: stdDeviation=”${Animations.Filter.stdDeviationx_${line}} ${Animations.Filter.stdDeviationy_${line}}”. In the corresponding toml file with default parameters, there must be a [defaults] section, containing a value for stdDeviationx and stdDeviationy.

[defaults]
stdDeviationx = 3
stdDeviationy = 3
colorInterpolationFilters = "sRGB" # or linearRGB

SVG filters are a very deep topic. You can learn more about SVG filters online, e.g. https://www.smashingmagazine.com/2015/05/why-the-svg-filter-is-awesome/ An interesting web tool to help you develop your own SVG filters, is https://yoksel.github.io/svg-filters/#/

Pro-tip: the inkscape source code has a file called filters.svg which contains over 200 filters which all can be ported to camala. Also on this webpage there’s a big collection of filters: http://srufaculty.sru.edu/david.dailey/svg/text/

Using RawSvgDefs, RawSvgElementsUnder and RawSvgElementsOver

Camala doesn’t support every feature SVG supports.

To inject a raw svg definition, you can use the ‘RawSvgDefs’ section. This can e.g. be used to define a gradient. To inject raw svg elements outside the <defs> </defs> section, you can use ‘RawSvgElementsUnder’ (these are drawn before the text, so under the text) and ‘RawSvgElementsOver’ (drawn after the text, so on top of it). Note that the RawSvgXxx sections cannot be animated at this moment.

An example can be to use it to generate a gradient and some rectangles, as shown below:

_images/gradient.gif
[Global]
W = "1000"
H = "500"
duration = "1"
fps = "25"
format = "gif"
background = "black"

[Animations]

[RawSvgDefs.def1]
def = """
<linearGradient id="MyGradient">
    <stop offset="5%" stop-color="#F60"/>
    <stop offset="95%" stop-color="#FF6"/>
</linearGradient>
"""

[RawSvgDefs.def2]
def = """
<radialGradient id="MyGradient2" gradientUnits="userSpaceOnUse" cx="400" cy="200" r="300" fx="400" fy="200">\
    <stop offset="0%" stop-color="red"/>
    <stop offset="50%" stop-color="blue"/>
    <stop offset="100%" stop-color="red"/>
</radialGradient>
"""

[RawSvgDefs.def3]
def = """
<pattern id="star" viewBox="0,0,10,10" width="1%" height="10%" style="fill:yellow;">
  <polygon points="0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2" />
</pattern>
"""

[Styles.normal.StyleProperties]
text-anchor="middle"
fill="url(#MyGradient)"
stroke="black"
stroke-width="0.5px"
font-size="50"
font-family="sans-serif"

[Styles.normal2.StyleProperties]
text-anchor="middle"
fill="url(#MyGradient2)"
stroke="black"
stroke-width="0.5px"
font-size="30"
font-family="sans-serif"

[Styles.stars.StyleProperties]
text-anchor="middle"
fill="url(#star)"
stroke="yellow"
stroke-width="1px"
font-size="150"
font-family="sans-serif"

[RawSvgElementsUnder.element1]
element = """
<rect fill="url(#MyGradient2)" stroke="black" stroke-width="5" x="-500" y="-50" width="1000" height="100"/>
"""

[RawSvgElementsOver.element1]
element = """
<rect fill="url(#MyGradient)" stroke="black" stroke-width="5" x="-300" y="100" width="600" height="200"/>
"""


[Caption.Line1]
pos = "[0, 0]"
[Caption.Line1.Segments.Segment1]
text= "Simple caption on top of rectangle."
style = "${Styles.normal}"
[Caption.Line2]
pos = "[0, 100]"
[Caption.Line2.Segments.Segment1]
text = "A smaller caption overwritten by rectangle."
style = "${Styles.normal2}"
[Caption.Line3]
pos = "[0, -100]"
[Caption.Line3.Segments.Segment1]
text = "PATTERNS?!"
style = "${Styles.stars}"

Examples from real life videos

_images/howtomakeapianosing.gif
[Global]
W = "1920"
H = "1080"
duration = "10"
fps = "30"
format = "gif"
background = "black"

[Animations]
    [Animations.Position]
        [Animations.Position.top_to_bottom]
            type = "PointAnimation"
            begin = "[0, -${Global.H}/1.5]"
            end = "[0, 0]"
            tween = "easeOutBounce"
            ytween = "easeOutBounce"
        [Animations.Position.bottom_to_top]
            type = "PointAnimation"
            begin = "[0, ${Global.H}*1.5]"
            end = "[0, ${Global.H}/4]"
            tween = "easeOutBounce"
            ytween = "easeOutBounce"
        [Animations.Position.dontmove]
            type = "PointAnimation"
            begin = "[0,100]"
            end = "[0,100]"
            tween = "linear"
            ytween = "linear"
    [Animations.Style]
        [Animations.Style.grow]
            type = "NumberAnimation"
            begin = "0"
            end = "100"
            tween = "easeOutBounce"
        [Animations.Style.shrink]
            type = "NumberAnimation"
            begin = "100"
            end = "0"
            tween = "easeOutQuad"
        [Animations.Style.shrink2]
            type = "NumberAnimation"
            begin = "500"
            end = "0"
            tween = "easeOutQuad"
        [Animations.Style.subsubtitle]
            type = "SequentialAnimation"
            elements = "[${Animations.Style.grow}, ${Animations.Style.shrink}]"
            time_weights = "[0.5, 1.0]"
            repeats = "3"

[Styles]
    [Styles.title]
        [Styles.title.StyleProperties]
            text-anchor="middle"
            fill="white"
            stroke="black"
            stroke-width="2px"
            font-size="80"
            font-family="Bitstream Vera Sans"
    [Styles.subtitle]
        [Styles.subtitle.StyleProperties]
            text-anchor="middle"
            fill="white"
            stroke="black"
            stroke-width="2px"
            font-size="60"
            font-family="Bitstream Vera Sans"
            font-style="oblique"
    [Styles.subsubtitle]
        [Styles.subsubtitle.StyleProperties]
            text-anchor="middle"
            fill="black"
            stroke="white"
            stroke-width="2px"
            font-size="50"
            font-family="Bitstream Vera Sans"
            font-style="bold"
            letter-spacing="${Animations.Style.shrink2}"
        [Styles.subsubtitle.StyleAnimation.subsubtitle]
            begin_time = "${Global.duration}*2/3"
            end_time = "${Global.duration}"
        [Styles.subsubtitle.StyleAnimation.shrink2]
            begin_time = "${Global.duration}*2/3"
            end_time = "${Global.duration}*3/4"

[Caption]
    [Caption.Line1]
        pos = "${Animations.Position.top_to_bottom}"
        [Caption.Line1.PositionAnimation]
            birth_time = "0"
            begin_time = "0"
            end_time = "${Global.duration}/3"
            death_time = "${Global.duration}"
        [Caption.Line1.Segments]
            [Caption.Line1.Segments.Segment1]
                text= "How to make a piano sing."
                style = "${Styles.title}"
    [Caption.Line2]
        pos = "${Animations.Position.bottom_to_top}"
        [Caption.Line2.PositionAnimation]
            birth_time = "0"
            begin_time = "${Global.duration}/3"
            end_time = "${Global.duration}*2/3"
            death_time = "${Global.duration}"
        [Caption.Line2.Segments]
            [Caption.Line2.Segments.Segment1]
                style = "${Styles.subtitle}"
                text= "No. Really."
    [Caption.Line3]
        pos = "${Animations.Position.dontmove}"
        [Caption.Line3.PositionAnimation]
            birth_time = "${Global.duration}*2/3"
            begin_time = "${Global.duration}*2/3"
            end_time = "${Global.duration}*5/6"
            death_time = "${Global.duration}"
        [Caption.Line3.Segments]
            [Caption.Line3.Segments.Segment1]
                text = "An empirical study."
                style = "${Styles.subsubtitle}"
_images/thisvideomaycontaintracesofmath.gif
[Global]
W = "1920"
H = "1080"
duration = "3"
fps = "30"
format = "gif"
background = "black"

[Animations]
    #[Animations.Style]
        [Animations.Style.grow]
            type = "NumberAnimation"
            begin = "0"
            end = "100"
            tween = "easeOutBounce"
[Styles]
    [Styles.h3_grow1]
        [Styles.h3_grow1.StyleProperties]
            text-anchor="middle"
            fill="white"
            stroke="black"
            stroke-width="1px"
            font-family="Bitstream Vera Sans"
            font-size="${Animations.Style.grow}"
        [Styles.h3_grow1.StyleAnimation]
            [Styles.h3_grow1.StyleAnimation.grow]
                begin_time = '0'
                end_time = '${Global.duration}-1.2'
    [Styles.h3_grow2]
        [Styles.h3_grow2.StyleProperties]
            text-anchor="middle"
            fill="white"
            stroke="black"
            stroke-width="1px"
            font-family="Bitstream Vera Sans"
            font-size="${Animations.Style.grow}"
        [Styles.h3_grow2.StyleAnimation]
            [Styles.h3_grow2.StyleAnimation.grow]
                begin_time = '0.1'
                end_time = '${Global.duration}-1.1'
    [Styles.h3_grow3]
        [Styles.h3_grow3.StyleProperties]
            text-anchor="middle"
            fill="#ff7c84"
            stroke="black"
            stroke-width="1px"
            font-family="Bitstream Vera Sans"
            font-style="oblique"
            font-size="${Animations.Style.grow}"
        [Styles.h3_grow3.StyleAnimation]
            [Styles.h3_grow3.StyleAnimation.grow]
                begin_time = '0.2'
                end_time = '${Global.duration}-1'
[Caption]
    [Caption.Line1]
        pos = "[0, -120]"
        [Caption.Line1.Segments]
            [Caption.Line1.Segments.Segment1]
                style = "${Styles.h3_grow1}"
                text = "Warning: this video may"
    [Caption.Line2]
        pos = "[0, 0]"
        [Caption.Line2.Segments]
            [Caption.Line2.Segments.Segment1]
                style = "${Styles.h3_grow2}"
                text = "contain tiny traces of math."
    [Caption.Line3]
        pos = "[0, 140]"
        [Caption.Line3.Segments]
            [Caption.Line3.Segments.Segment1]
                style = "${Styles.h3_grow3}"
                text = "Math intolerant, beware!"