diff --git a/mod_graph/anim1.py b/mod_graph/anim1.py
new file mode 100644
index 0000000..4099bb8
--- /dev/null
+++ b/mod_graph/anim1.py
@@ -0,0 +1,131 @@
+"""
+    Анимация квадратов
+    https://stepik.org/lesson/468998/step/4?unit=459819
+
+    3: "Квадрат двигается справа налево"
+    4: "Два квадрата двигаются в противоположных направлениях"
+
+    Библиотека graph: https://kpolyakov.spb.ru/download/pygraph.zip
+"""
+
+from graph import *
+
+from typing import NamedTuple
+
+
+class Point(NamedTuple):
+    """
+    Вспомогательный класс для упрощения работы с координатами (сложение)
+    """
+    
+    x: int | float
+    y: int | float
+
+    def __add__(self, other):
+        if isinstance(other, self.__class__):
+            return self.__class__(self.x + other.x, self.y + other.y)
+        if isinstance(other, (int, float)):
+            return self.__class__(self.x + other, self.y + other)
+        return NotImplemented
+
+
+def make_title(n, name, start, font=("Arial", 15), n_color="indigo"):
+    pos = Point(10, 20) + start
+    n_width = Point(35, 0)
+    l1 = label(f"«{n}»", *pos, font=font, foreground=n_color)
+    l2 = label(f": {name}", *(pos + n_width), font=font)
+    return l1, l2
+
+
+def square_left_to_right(start_pos=None):
+    start_pos = start_pos or Point(0, 0)
+
+    make_title(3, "Квадрат двигается справа налево", start_pos)
+    title_height = Point(0, 60)
+
+    start_pos = start_pos + title_height
+    scene_sz = Point(300, 300)
+    box_sz = Point(40, 40)
+    box_offset = Point(220, 5)
+
+    brushColor("Blue")
+    rectangle(*start_pos, *(start_pos + scene_sz))  # Размер фона
+    brushColor("Yellow")
+    obj = rectangle(
+        *(start_pos + box_offset), *(start_pos + box_offset + box_sz)
+    )  # Создаем объект, который у нас в виде прямоугольника
+
+    data = {"done": False}  # состояние объекта для контроля таймера из вне функции
+
+    def update():  # Создаем процедуру, которая отвечает за анимацию
+        # Она будет перемещать объект obj по оси x на -5 по оси y на 0 пикселей
+        moveObjectBy(obj, -5, 0)
+        if xCoord(obj) <= 1:  # Если x координата меньше 1
+            killTimer(timer)
+            data["done"] = True
+
+    timer = onTimer(update, 40)  # Каждые 50 мс будет запускаться процедура update
+    return (data,)
+
+
+def two_squares(start_pos=None):
+    start_pos = start_pos or Point(0, 0)
+
+    make_title(4, "Два квадрата двигаются в\nпротивоположных направлениях", start_pos)
+    title_height = Point(0, 90)
+
+    start_pos = start_pos + title_height
+    scene_sz = Point(300, 300)
+    box_sz = Point(40, 40)
+    box1_offset = Point(220, 5)
+    box2_offset = Point(0, 250)
+
+    brushColor("Blue")
+    rectangle(*start_pos, *(start_pos + scene_sz))
+    brushColor("Yellow")
+    box1 = rectangle(*(start_pos + box1_offset), *(start_pos + box1_offset + box_sz))
+    box2 = rectangle(*(start_pos + box2_offset), *(start_pos + box2_offset + box_sz))
+    timer1 = None
+    timer2 = None
+
+    data1 = {"done": False}
+    data2 = {"done": False}
+
+    def update_box1():
+        moveObjectBy(box1, -5, 0)
+        if xCoord(box1) <= 1:
+            killTimer(timer1)
+            data1["done"] = True
+
+    def update_box2():
+        moveObjectBy(box2, 5, 0)
+        if xCoord(box2) >= scene_sz.x - box_sz.x:
+            killTimer(timer2)
+            data2["done"] = True
+
+    timer1 = onTimer(update_box1, 40)
+    timer2 = onTimer(update_box2, 40)
+
+    return data1, data2
+
+
+def main():
+    canvasSize(400, 800)
+    state = [
+        *square_left_to_right(), 
+        *two_squares(Point(0, 350))
+    ]
+
+    watch_timer = None
+
+    # выход по завершении всех анимаций
+    def watch_done():
+        if all(x["done"] for x in state):
+            killTimer(watch_timer)
+            close()
+
+    watch_timer = onTimer(watch_done, 6000)
+    run()
+
+
+main()