Chapter 14. Animation

draw_picture.py contains a call to _anim_shapes() which shows how to animate a circle and a line. There’s a second animation example in anim_bicycle.py which translates and rotates a bicycle image. The chapter ends with a brief outline of the com.sun.star.gallery module.

14.1 Animating a Circle and a Line

_anim_shapes() in draw_picture.py implements two animation loops that work in a similar manner. Inside a loop, a shape is drawn, the function (and program) sleeps for a brief period, then the shape’s position, size, or properties are updated, and the loop repeats.

The first animation loop moves a circle across the page from left to right, reducing its radius at the same time. The second loop rotates a line counter-clockwise while changing its length. The _anim_shapes() code:

# from draw_picture.py
def _anim_shapes(self, curr_slide: DrawPage[DrawDoc]) -> None:
    xc = 40
    yc = 150
    radius = 40
    circle = None
    for _ in range(20):
        # move right
        if circle is not None:
            curr_slide.remove(circle.component)
        circle = curr_slide.draw_circle(x=xc, y=yc, radius=radius)

        Lo.delay(200)
        xc += 5
        radius *= 0.95

    x2 = 140
    y2 = 110
    line = None
    for _ in range(25):
        if line is not None:
            curr_slide.remove(line.component)
        line = curr_slide.draw_line(x1=40, y1=100, x2=x2, y2=y2)
        x2 -= 4
        y2 -= 4

The shape (circle or line) is changed by removing the current version from the page and inserting a new updated instance. This means that a lot of objects are created and removed in a short amount of time. The alternative approach, which retains the shape and only update its properties, is used in the bicycle animation explained next.

14.2 Animating an Image

The Animate Bike example moves a bicycle image to the right and rotates it counter-clockwise. Fig. 120 shows the page after the animation has finished.

Animated Bicycle and Shapes.

Fig. 120 :Animated Bicycle and Shapes.

The animation is performed by _animate_bike():

# from anim_bicycle.py
def _animate_bike(self, slide: DrawPage[DrawDoc]) -> None:
    shape = slide.draw_image(fnm=self._fnm_bike, x=60, y=100, width=90, height=50)

    pt = shape.get_position_mm()
    angle = shape.get_rotation()
    print(f"Start Angle: {int(angle)}")
    for i in range(19):
        shape.set_position(x=pt.X + (i * 5), y=pt.Y)  # move right
        shape.set_rotation(angle=angle + (i * 5))  # rotates ccw
        Lo.delay(200)

    print(f"Final Angle: {int(shape.get_rotation())}")
    Draw.print_matrix(shape.get_transformation())

The animation loop in _animate_bike() is similar to the ones in anim_shapes(), using Lo.delay() to space out changes over time. However, instead of creating a new shape on each iteration, a single GraphicObjectShape is created by slide.draw_image() which invokes Draw.draw_image() before the loop starts. Inside the loop, that shape’s position and orientation are repeatedly updated by shape.set_position() and shape.set_rotation() which invokes Draw.set_position() and Draw.set_rotation() respectively.

14.2.1 Drawing the Image

There are several versions of Draw.draw_image() the main one is:

# represents draw_image() overloads in Draw Class (simplified)
@classmethod
def draw_image(cls, slide: XDrawPage, fnm: PathOrStr) -> XShape:
    slide_size = cls.get_slide_size(slide)
    im_size = ImagesLo.get_size_100mm(fnm)
    im_width = round(im_size.Width / 100)  # in mm units
    im_height = round(im_size.Height / 100)
    x = round((slide_size.Width - im_width) / 2)
    y = round((slide_size.Height - im_height) / 2)
    return cls.draw_image(slide=slide, fnm=fnm, x=x, y=y, width=im_width, height=im_height)

@classmethod
def draw_image(
    cls,
    slide: XDrawPage,
    fnm: PathOrStr,
    x: int | UnitT,
    y: int | UnitT,
    width: int | UnitT,
    height: int | UnitT
) -> XShape:

    # units in mm's
    Lo.print(f'Adding the picture "{fnm}"')
    im_shape = cls.add_shape(
        slide=slide,
        shape_type=DrawingShapeKind.GRAPHIC_OBJECT_SHAPE,
        x=x,
        y=y,
        width=width,
        height=height
    )
    cls.set_image(im_shape, fnm)
    cls.set_line_style(shape=im_shape, style=LineStyle.NONE)
    return im_shape

draw_image() uses the supplied (x, y) position, width, and height to create an empty GraphicObjectShape. An image is added by setImage(), which loads a bitmap from a file, and assigns it to the shape’s GraphicURL property. By using a bitmap, the image is embedded in the document.

Alternatively, a URL could be assigned to GraphicURL, causing the document’s image to be a link back to its original file.

That version is coded using:

Props.set(GraphicURL=FileIO.fnm_to_url(im_fnm))

A second version of Draw.draw_image() doesn’t require width and height arguments – they’re obtained from the image’s dimensions:

# represents draw_image() overload in Draw Class (simplified)
@classmethod
def draw_image(
    cls,
    slide: XDrawPage,
    fnm: PathOrStr,
    x: int | UnitT,
    y: int | UnitT,
) -> XShape:
    im_size = ImagesLo.get_size_100mm(fnm)
    return cls.draw_image(
        slide=slide,
        fnm=fnm,
        x=x,
        y=y,
        width=round(im_size.Width / 100),
        height=round(im_size.Height / 100)
    )

The image’s size is returned in 1/100 mm units by ImagesLo.get_size_100mm(). It loads the image as an XGraphic object so that its Size100thMM property can be examined:

# in the ImagesLo class
@classmethod
def get_size_100mm(cls, im_fnm: PathOrStr) -> Size:
    graphic = cls.load_graphic_file(im_fnm)
    return mProps.Props.get(graphic, "Size100thMM")

This approach isn’t very efficient since it means that the image is being loaded twice, once as an XGraphic object by get_size_100mm(), and also as a bitmap by setImage().

14.2.2 Updating the Bike’s Position and Orientation

The _animate_bike() animation uses Draw methods for getting and setting the shape’s position and orientation:

# in the Draw Class (simplified)
@staticmethod
def get_position(shape: XShape) -> Point:
    pt = shape.getPosition()
    # convert to mm
    return Point(round(pt.X / 100), round(pt.Y / 100))

# one of several overloads
@staticmethod
def set_position(shape: XShape, x: int, y: int) -> None:
    shape.set_position(Point(x * 100, y * 100))

@staticmethod
def get_rotation(shape: XShape) -> Angle:
    r_angle = int(mProps.Props.get(shape, "RotateAngle"))
    return Angle(round(r_angle / 100))

@staticmethod
def set_rotation(shape: XShape, angle: Angle) -> None:
    mProps.Props.set(shape, RotateAngle=angle.Value * 100)

The position is accessed and changed using the XShape methods get_position() and set_position(), with the only complication being the changes of millimeters into 1/100 mm units, and vice versa.

Rotation is handled by getting and setting the shape’s RotateAngle property, which is inherited from the RotationDescriptor class. The angle is expressed in 1/100 of a degree units (e.g. 4500 rather than 45 degrees), and a positive rotation is counter-clockwise.

One issue is that RotationDescriptor is deprecated; the modern programmer is encouraged to rotate a shape using the matrix associated with the Transformation property.

The Draw class has are two support functions for Transformation: one extracts the matrix from a shape, and the other prints it:

# in the Draw Class (simplified)
@staticmethod
def get_transformation(shape: XShape) -> HomogenMatrix3:
    return mProps.Props.get(shape, "Transformation")

@staticmethod
def print_matrix(mat: HomogenMatrix3) -> None:
    print("Transformation Matrix:")
    print(f"\t{mat.Line1.Column1:10.2f}\t{mat.Line1.Column2:10.2f}\t{mat.Line1.Column3:10.2f}")
    print(f"\t{mat.Line2.Column1:10.2f}\t{mat.Line2.Column2:10.2f}\t{mat.Line2.Column3:10.2f}")
    print(f"\t{mat.Line3.Column1:10.2f}\t{mat.Line3.Column2:10.2f}\t{mat.Line3.Column3:10.2f}")

    rad_angle = math.atan2(mat.Line2.Column1, mat.Line1.Column1)
    #       sin(t), cos(t)
    curr_angle = round(math.degrees(rad_angle))
    print(f"  Current angle: {curr_angle}")
    print()

These methods are called at the end of _animate_bike():

# from anim_bicycle.py _animate_bike()
Draw.print_matrix(shape.get_transformation())

The output is:

Transformation Matrix:
          0.00         5001.00        15383.00
      -9001.00            0.00        10235.00
          0.00            0.00            1.00
  Current angle: -90

These numbers suggests that the transformation was a clockwise rotation, but the calls to Draw.set_rotation() in the earlier animation loop made the bicycle turn counter-clockwise. This discrepancy pointed to stay with the deprecated approach for shape rotation.