Анимации в DrawZero
Введение
Эта страница объясняет, как создавать плавные анимации в DrawZero. Мы используем очень простой язык и много примеров, чтобы даже ученики средней школы, для которых английский не является родным языком, могли все понять. К концу вы узнаете, как управлять циклом анимации, как рисовать каждый кадр, как поддерживать стабильную частоту кадров и как добавлять дополнительные визуальные эффекты, такие как следы движения.
Цикл событий анимации
Все анимации в DrawZero следуют одному и тому же ритму. Типичный цикл выглядит так:
while True:
tick()
clear()
# здесь что-то рисуем
Давайте рассмотрим каждый шаг в цикле.
tick()делает паузу, чтобы поддерживать постоянную частоту кадров, и собирает новые события ввода с клавиатуры и мыши.clear()очищает экран перед тем, как мы нарисуем следующий кадр. Вы можете заменить его другими трюками (например, прозрачными заливками), когда хотите получить след движения.- После этого вы рисуете фигуры, текст или изображения для этого кадра.
Когда цикл доходит до конца, он начинается снова. Это повторяется много раз в секунду, что и создает анимацию.
Почему важен порядок
Всегда вызывайте tick() перед рисованием. Функция обновляет внутренний таймер и
обрабатывает события окна. Если вы пропустите ее, окно может зависнуть, и кнопка
закрытия может перестать работать. Очистка экрана после tick(), но перед
рисованием, гарантирует, что новый кадр не смешается с предыдущим (если только вы
не хотите этого намеренно для эффекта следа).
Полный первый пример
Вот крошечная программа, которая перемещает круг слева направо:
from drawzero import *
x = 50
speed = 5
while True:
tick() # поддерживаем 30 кадров в секунду и читаем события
clear() # стираем предыдущий кадр
circle('orange', (x, 300), 40, line_width=4)
filled_circle('yellow', (x, 300), 32)
x += speed # изменяем позицию для следующего кадра
if x > 1100:
x = 50
Запустите ее, и вы увидите, как круг движется по экрану. Попробуйте изменить скорость или размер, чтобы поэкспериментировать.
Справочник по функциям
clear()
def clear():
"""Сбрасывает экран до черного цвета."""
clear() очищает холст до сплошного черного цвета. Обычно вы вызываете ее один раз за
кадр, чтобы начать с чистой поверхности. Это самый быстрый способ удалить все,
что было нарисовано в предыдущем кадре.
Совет: если вы предпочитаете другой цвет фона, вы можете нарисовать
заполненный прямоугольник, который покрывает все окно, сразу после clear().
fill(color='red', alpha=255)
fill() покрывает экран цветом. Параметр alpha управляет
прозрачностью. Когда alpha меньше 255, новый цвет становится
полупрозрачным. Вы можете использовать этот трюк вместо clear(), чтобы
сохранить следы движения:
while True:
tick()
fill('black', alpha=30) # почти очищаем, но оставляем мягкий след
# здесь рисуем движущиеся объекты
Низкое значение альфа-канала (например, 30) дает длинный след, потому что старый
рисунок исчезает медленно. Более высокое значение альфа-канала (например, 200)
стирает кадр почти как полный clear().
tick(r=1)
def tick(r=1):
"""Приостанавливает выполнение на 1/30 секунды.
Если функция tick вызывается в цикле, то время сна уменьшается так,
чтобы между вызовами проходило 1/30 секунды. Например, если вычисления между вызовами tick() занимают 1/60 с,
то tick() спит 1/60 с. Таким образом, пока вычисления занимают меньше 1/30 с,
мы получаем 30 кадров в секунду."""
Вызов tick() поддерживает анимацию на скорости 30 кадров в секунду (FPS). Это
работает только тогда, когда ваши собственные рисунки и вычисления достаточно
быстры. Если одна итерация цикла занимает меньше 1/30 секунды, tick() спит
оставшуюся долю, так что общее время между кадрами составляет почти ровно 1/30
секунды. Пока вы остаетесь в этом пределе, анимация плавная и стабильная.
Необязательный параметр r позволяет вам продвигаться на несколько тиков сразу.
DrawZero выполнит внутреннее обновление кадра r раз подряд. Это полезно,
когда вы хотите ускорить симуляцию без перерисовки между кадрами. Большинство
программ оставляют значение по умолчанию 1.
Помимо тайминга, tick() очищает очередь событий окна. Он собирает все новые
движения мыши, нажатия кнопок и клавиш, чтобы вы могли прочитать их из
глобальных списков keysdown, keysup, mousemotions, mousebuttonsdown,
которые появляются после from drawzero import *. Если вы пропустите tick(),
вы не увидите ввод пользователя, и окно может перестать отвечать.
sleep(t=1)
def sleep(t=1):
"""Приостанавливает выполнение на t секунд
:param t: Количество секунд для сна
"""
sleep() приостанавливает программу на более длительное время. Внутренне это просто
цикл из t * 30 вызовов tick(), поэтому окно продолжает обрабатывать события.
Используйте это, когда вам нужен перерыв между фазами анимации. Пример:
показать текст на две секунды, прежде чем начать перемещать объекты.
clear()
text('white', 'Готов...', (400, 320), fontsize=48)
sleep(2)
fps(fontsize=24)
def fps(fontsize=24, *, prev=[time()]):
cur = time()
diff = cur - prev[0]
prev[0] = cur
rate = int(1 / diff + 0.5)
text('white', f'{rate} FPS', (1000, 000), fontsize, '>^')
Вызывайте fps() один раз за кадр, чтобы нарисовать небольшой счетчик в
верхнем правом углу. Он измеряет время между текущим и предыдущим вызовом,
преобразует его в кадры в секунду и выводит число на экран. Измените
fontsize, если вам нужен текст большего или меньшего размера.
Если число падает значительно ниже 30, ваш код анимации слишком медленный. Попробуйте уменьшить объем работы, который вы делаете каждый кадр, или рисовать меньше очень сложных фигур.
Работа с движением на основе кадров
Поскольку tick() поддерживает цикл на скорости 30 FPS, вы можете описывать движение в
"пикселях за кадр". Например, добавление 5 к позиции x каждый кадр означает,
что объект перемещается на 150 пикселей каждую секунду (30 * 5). Если вы хотите,
чтобы движение оставалось одинаковым даже при изменении частоты кадров,
измеряйте фактическую разницу во времени. Вы можете сохранить временную метку в
конце каждого цикла с помощью функции time() и масштабировать свое движение на
эту дельту.
from time import time
x = 100
speed_per_second = 200 # пикселей в секунду
prev_time = time()
while True:
tick()
now = time()
dt = now - prev_time
prev_time = now
clear()
x += speed_per_second * dt
filled_circle('cyan', (x, 300), 30)
Добавление следов с помощью прозрачных заливок
Чтобы создать следы, похожие на кометные, замените clear() на темную
прозрачную заливку. Старый рисунок будет медленно исчезать, а новый появится
сверху.
while True:
tick()
fill('black', alpha=20) # низкое значение альфа = длинный след
filled_circle('lime', pos, 20)
Вы также можете смешивать оба метода: вызывать clear() каждые несколько кадров,
чтобы сбросить экран, и использовать fill() с alpha между ними, чтобы
сохранить более короткий след.
Структурирование больших анимаций
Вот несколько советов, когда ваша анимация растет:
- Оберните свой код рисования в функции, чтобы цикл
while Trueоставался маленьким и легко читаемым. - Держите вычисления вне команд рисования. Сначала обновите позиции, затем нарисуйте все.
- Используйте
tick()только один раз за кадр. Если вы вызовете его несколько раз, ваша анимация замедлится, потому что функция может ждать после каждого вызова. - Помните, что
sleep()также вызываетtick()внутри. Это означает, что окно остается отзывчивым даже во время пауз. - Следите за счетчиком FPS во время разработки. Стабильное значение около 30 означает, что ваш цикл здоров.
Идеи для дальнейшей практики
- Сделайте прыгающий мяч, который меняет направление, когда ударяется о край окна.
- Нарисуйте змееподобный след, используя
fill('black', alpha=10)и движущийся круг. - Создайте ночное небо с движущимися звездами. Используйте разные скорости для
каждой звезды и вызывайте
fps(), чтобы убедиться, что цикл остается быстрым.
Поэкспериментируйте с приведенными выше примерами, комбинируйте функции, и вы быстро почувствуете, как работает цикл анимации DrawZero. Удачи в создании собственных анимированных историй!