Skip to content

Animation

sangfroid.Animation(filename=None) #

Bases: sangfroid.layer.Group

A Synfig animation. It can be loaded from a file in the .sif, .sifz, or .sfg formats.

Note

.sfg support is currently broken. See issue #2.

Synfig animations are made up of [sangfroid.layer.Layer][]s. Some of these layers, such as Animation itself, can contain other layers. Animation can only be the outermost layer, and it contains all the others. It also holds the list of keyframes, and some global settings, such as the frame speed and resolution.

To load a file:

import sangfroid

sif = sangfroid.Animation('fred.sif')

To start with a blank canvas:

import sangfroid

sif = sangfroid.Animation()

Attributes:

Name Type Description
antialias int

background_first_color sangfroid.value.color.Color

background_rendering sangfroid.value.simple.Integer

background_second_color sangfroid.value.color.Color

background_size sangfroid.value.vector.X_Y

begin_time sangfroid.t.T

The time at which this animation starts. Almost always zero.

bgcolor sangfroid.t.T

canvas_tag bs4.element.Tag

desc str

A description of this animation, so you know what it is when you find it again next year.

end_time sangfroid.t.T

The time at which this animation ends.

fps float

The number of frames per second. Usually 24. (Can this be non-integer?)

gamma_b float

gamma_g float

gamma_r float

grid_color sangfroid.value.color.Color

grid_show sangfroid.value.simple.Integer

grid_size sangfroid.value.vector.X_Y

grid_snap sangfroid.value.simple.Integer

guide_color sangfroid.value.color.Color

guide_show sangfroid.value.simple.Integer

guide_snap sangfroid.value.simple.Integer

height int

The height of the canvas, in pixels.

jack_offset sangfroid.value.simple.Real

name str

The name of this animation— not the filename, though it's often the same.

onion_skin sangfroid.value.simple.Integer

onion_skin_future sangfroid.value.simple.Integer

onion_skin_keyframes sangfroid.value.simple.Integer

onion_skin_past sangfroid.value.simple.Integer

version float

The Synfig version which (notionally) created this animation

view_box str

width int

The width of the canvas, in pixels.

xres float

The horizontal resolution.

yres float

The vertical resolution.

Parameters:

Name Type Description Default
filename str | None

the name of the main file to load. If this is None, we create a blank animation.

None
Source code in sangfroid/animation.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def __init__(self, filename:str|None=None):
    """
    Args:
        filename: the name of the main file to load.
                    If this is None, we create a blank animation.
    """
    self._filename = filename

    if filename is None:
        self._format = Blank()
    else:
        self._format = Format.from_filename(filename)

    with self._format.main_file() as soup:
        self._soup = soup

    assert len(self._soup.contents)==1
    tag = self._soup.contents[0]
    super().__init__(
            tag = tag,
            )

__call__ = find class-attribute instance-attribute #

Which Layer subclass handles which type of tag.

active = TagAttrField(bool, True) class-attribute instance-attribute #

True if this layer is enabled.

amount = f.ParamTagField(v.Real, 1.0) class-attribute instance-attribute #

1.0 for opaque, 0.0 for transparent.

begin_time = TagTimeAttrField(0) class-attribute instance-attribute #

The time at which this animation starts.

Almost always zero.

blend_method = f.ParamTagField(v.BlendMethod, v.BlendMethod.COMPOSITE) class-attribute instance-attribute #

How this layer will affect the layers beneath it.

canvas = f.ParamTagField(v.Canvas, []) class-attribute instance-attribute #

Any layers which are inside this one.

desc = NamedChildField(str, 'Animation') class-attribute instance-attribute #

A description of this animation, so you know what it is when you find it again next year.

end_time = TagTimeAttrField('5s') class-attribute instance-attribute #

The time at which this animation ends.

exclude_from_rendering = TagAttrField(bool, False, name='exclude_from_rendering') class-attribute instance-attribute #

True if this layer should not be rendered.

fps = TagAttrField(float, 24.0) class-attribute instance-attribute #

The number of frames per second. Usually 24.

(Can this be non-integer?)

framecount property #

The number of frames in this animation.

Should be equal to int(end_time)-int(begin_time).

Note that this is one higher than the number of the last frame.

height = TagAttrField(int, 270) class-attribute instance-attribute #

The height of the canvas, in pixels.

keyframes property #

The defined keyframes.

name = NamedChildField(str, 'Not yet named') class-attribute instance-attribute #

The name of this animation— not the filename, though it's often the same.

tag = TagField() class-attribute instance-attribute #

The BeautifulSoup tag behind this item.

transformation = f.ParamTagField(v.Transformation, {'offset': (0.0, 0.0), 'angle': 0.0, 'skew_angle': 0.0, 'scale': (1.0, 1.0)}) class-attribute instance-attribute #

How to move, rotate, skew, or scale the rendering of this canvas.

type_ = TypeNameField() class-attribute instance-attribute #

The name Synfig uses internally for this type of layer.

In Python, you must spell this as type_, because type is a reserved word.

version = TagAttrField(float, 1.2) class-attribute instance-attribute #

The Synfig version which (notionally) created this animation

width = TagAttrField(int, 480) class-attribute instance-attribute #

The width of the canvas, in pixels.

xres = TagAttrField(float, 2834.645669) class-attribute instance-attribute #

The horizontal resolution.

yres = TagAttrField(float, 2834.645669) class-attribute instance-attribute #

The vertical resolution.

z_depth = f.ParamTagField(v.Real, 0.0) class-attribute instance-attribute #

How deep in the group this layer appears.

find(*args, recursive=True, attrs=None, **kwargs) #

Like find_all(), except that it only returns the first item.

If no items are found, it returns None.

Arguments are as for find_all().

Source code in sangfroid/layer/layer.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def find(self,
         *args:(bool|str|Self|Callable),
         recursive:bool = True,
         attrs:(dict|None) = None,
         **kwargs,
         ) -> (Self|None):
    """
    Like find_all(), except that it only returns the first item.

    If no items are found, it returns None.

    Arguments are as for find_all().
    """
    items = self.find_all(
            *args,
            recursive=recursive,
            attrs=attrs,
            **kwargs,
            )
    if items:
        return items[0]
    else:
        return None

find_all(*args, recursive=True, attrs=None, **kwargs) #

Finds sub-layers with particular properties.

This can only usefully be called on Groups. On other layers, it does nothing.

Parameters:

Name Type Description Default
args bool | str | typing.Self | collections.abc.Callable

you may specify at most one positional argument. If it's True, all children will match. If it's False, no children will match. If it's a string, it will match on the "type" field. If it's the Layer class or one of its subclasses, it will match layers of that type. If it's a callable, it will be called for each child; if it returns True, the child will be returned, and otherwise it won't.

()
recursive bool

if this is False, we only search the layer's immediate children; if it's True, which is the default, we search all the layer's descendants.

True
attrs dict | None

what to search for. We match against the field with the given name. The values should be strings, except that "type" can also be the class Layer or one of its subclasses.

None

You may supply extra kwargs, under the same terms as "attrs"; you may not specify the same key in both.

The format of the arguments is based on Beautiful Soup's Tag.find_all() method.

Source code in sangfroid/layer/layer.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def find_all(self,
             *args:(bool|str|Self|Callable),
             recursive:bool = True,
             attrs:(dict|None) = None,
             **kwargs,
             ) -> [Self]:
    """
    Finds sub-layers with particular properties.

    This can only usefully be called on Groups. On other
    layers, it does nothing.

    Args:
        args: you may specify at most one positional argument.
            If it's True, all children will match.
            If it's False, no children will match.
            If it's a string, it will match on the "type" field.
            If it's the Layer class or one of its subclasses,
                it will match layers of that type.
            If it's a callable, it will be called for each
                child; if it returns True, the child will be
                returned, and otherwise it won't.

        recursive: if this is False, we only search
            the layer's immediate children; if it's True,
            which is the default, we search all the layer's
            descendants.

        attrs: what to search for. We match against the
            field with the given name. The values should be
            strings, except that "type" can also be the class
            Layer or one of its subclasses.

    You may supply extra kwargs, under the same terms as
    "attrs"; you may not specify the same key in both.

    The format of the arguments is based on Beautiful Soup's
    Tag.find_all() method.
    """

    matching_special = None

    if len(args)>1:
        raise ValueError(
                "You can only give one positional argument.")
    elif len(args)==1:

        if (
                isinstance(args[0], str) or
                (isinstance(args[0], type) and
                 issubclass(args[0], Layer))
                ):
            if 'type' in kwargs:
                raise ValueError(
                        "You can't give a type in both the positional "
                        "and keyword arguments.")

            kwargs['type'] = args[0]

        elif isinstance(args[0], bool):
            matching_special = args[0]

        elif hasattr(args[0], '__call__'):
            matching_special = args[0]

        else:
            raise TypeError(args[0])

    if 'attrs' in kwargs:
        for k,v in kwargs['attrs'].items():
            if k in kwargs:
                raise ValueError("{k} specified both as a kwarg and in attrs")
            kwargs[k] = v

        del kwargs['attrs']

    for k,v in kwargs.items():
        if k=='type':
            if not isinstance(v, str):
                v = v.__name__

            kwargs[k] = v.lower().replace('_', '')

    logger.debug("begin find_all")

    def matcher(found_tag):
        if found_tag.name!='layer':
            return False

        logger.debug("considering tag: %s %s",
                     found_tag.name, found_tag.attrs)

        found_layer = Layer.from_tag(found_tag)

        if matching_special is None:

            def munge(k,v):
                if k=='type':
                    k = 'type_'
                    v = v.lower()

                return (k,v)

            targets = [
                munge(k,v)
                for k,v in kwargs.items()
                ]

            logger.debug("want: %s", targets)

            for k, want_value in targets:
                try:
                    found_value = getattr(found_layer, k)
                    logger.debug("  -- %s field is %s; want %s", k,
                                 repr(found_value),
                                 repr(want_value),
                                 )
                except AttributeError:
                    logger.debug("  -- it does not have a %s", k)
                    continue

                logger.debug("  want: %s  found: %s",
                             want_value, found_value)

                if found_value==want_value:
                    logger.debug("    -- a match!")
                    return True

            logger.debug("  -- no matches.")
            return False

        elif isinstance(matching_special, bool):
            return matching_special

        else:
            result = matching_special(found_tag)
            logger.debug("  -- callback says: %s", result)
            return result

        raise ValueError(found_tag)

    result = [
            self.from_tag(x) for x in
            self._tag.find_all(matcher,
                              recursive=recursive,
                              )
            ]
    logger.debug("find_all found: %s",
                 result,
                 )

    return result

from_tag(tag) classmethod #

Constructs a layer from an XML tag.

Source code in sangfroid/layer/layer.py
336
337
338
339
340
341
342
343
344
345
@classmethod
def from_tag(cls, tag:bs4.Tag) -> Self:
    """
    Constructs a layer from an XML tag.
    """
    tag_type = tag.get('type', None)
    if tag_type is None:
        raise ValueError(
                f"tag has no 'type' field: {tag}")
    return cls.handles_type.from_name(name=tag_type)(tag)

save(filename=None) #

Saves the animation back out to disk.

Parameters:

Name Type Description Default
filename str | None

the filename to save the animation to. If None, we use the filename we loaded it from.

None
Source code in sangfroid/animation.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def save(self, filename:str|None=None):
    """
    Saves the animation back out to disk.

    Args:
        filename: the filename to save the animation to.
            If None, we use the filename we loaded it from.
    """

    if filename is None:
        if self._format is None:
            raise ValueError(
                    "If you didn't give a filename at creation, "
                    "you must give one when you save."
                    )
        filename = self._format.filename
    else:
        new_format = Format.from_filename(filename,
                                          load = False,
                                          )
        if new_format!=self._format:
            # XXX copy the images over
            self._format = new_format

    self._format.save(
            content = self._soup,
            filename = filename,
            )