29. Списки и геометрия, забавные задачи

Продолжим работать со списками.
Теперь шарики не выстроены в ряд, а раскиданы по экрану.

Добавим немного геометрии в наши задания. Некоторые задания просто забавны, другие же являются частью будущих игр.

Задания выполнять в функции task, вызывать ее нажатием правой кнопки мыши на экране.
Левая — очищает и создает новый список.

from random import randrange as rnd, choice
from tkinter import *

root = Tk()
root.geometry('800x600')

canv = Canvas(root, bg = 'white')
canv.pack(fill = BOTH, expand = 10)

colors = ['red','brown','green','lightgreen','yellow','black','blue']

def fill_random(event):
    global balls
    canv.delete(ALL)
    balls = []
    for z in range(12):
        balls += [ball()]

def paint():
    for b in balls:
        b.paint()

class ball():
    def __init__(self):
        x = self.x = rnd(50,700)
        y = self.y = rnd(50,550)
        r = self.r = rnd(10,30)
        self.color = choice(colors)
        self.pen_color = choice(colors)
        self.width = 0
        self.id = canv.create_oval(x-r,y-r,x+r,y+r, width = self.width, fill = self.color, outline = self.pen_color )
        self.paint()

    def paint(self):
        x = self.x
        y = self.y
        r = self.r
        canv.coords(self.id,x-r,y-r,x+r,y+r)
        canv.itemconfig (self.id, fill = self.color, width = self.width, outline = self.pen_color)

    def kill(self):
        canv.delete(self.id)
        balls.remove(self)

def task(event):
    # писать код здесь! Вызывать правой кнопкой мыши
    balls[-1].kill()
    pass

fill_random(0)
canv.bind('<Button-1>', fill_random)
canv.bind('<Button-3>',task)

mainloop()

Рассмотрим некоторые приемы, которые понадобятся вам, чтобы решить задачи, приведенные ниже.

Найти самый большой шарик и сделать его оранжевым:

def task(event):
    # писать код здесь! Вызывать правой кнопкой мыши
    bm = balls[0]
    for b in balls:
        if b.r > bm.r:
            bm = b
    canv.itemconfig(bm.id, fill = 'orange')

Провести линии от выбранной точки ко всем шарикам:

def task(event):
    x = event.x
    y = event.y
    for b in balls:
        canv.create_line(x,y,b.x,b.y, fill = 'orange', width = 2)

Сложнее, если линии нужно не только проводить, но и стирать. Для этого мы можем использовать два подхода: стирать все и рисовать заново и шарики и линии, или стирать только линии. Сначала рассмотрим то, что проще: стирать все.

def task(event):
    canv.delete(ALL)
    x = event.x
    y = event.y
    for b in balls:
        b.id = canv.create_oval(b.x-b.r,b.y-b.r,b.x+b.r,b.y+b.r, width = 0, fill = b.color )

    for b in balls:
        canv.create_line(x,y,b.x,b.y, fill = 'orange', width = 2)

Но это плохой путь. Лучше оставить перерисовку шариков самим шарикам и не удалять все.
Попробуем удалять только линии. Для этого, при создании линий нужно добавить tag (метку), чтобы потом можно было удалить элементы только с этой меткой:

def task(event):
    canv.delete('lines')
    x = event.x
    y = event.y

    for b in balls:
        canv.create_line(x,y,b.x,b.y, fill = 'orange', width = 2, tag = 'lines')
 

Теперь, если заменить событие на , то получим постоянное перемещние точки, из которой выпускаются лучи:

 def task(event):
    canv.delete('lines')
    x = event.x
    y = event.y
    for b in balls:
        canv.create_line(x,y,b.x,b.y, fill = 'orange', width = 2, tag = 'lines')
canv.bind('<Button-1>', fill_random)
canv.bind('<Motion>',task)

Перед тем, как будут созданы шарики, перемещение мыши вызывает ошибки. Сделаем так, чтобы по щелчку правой кнопкой включался режим рисования линий и выключался повторным щелчком правой кнопкой:

def line_on(event):
    canv.bind('<Button-3>',line_off)
    canv.bind('<Motion>',task)

def line_off(event):
    canv.bind('<Button-3>',line_on)
    canv.unbind('<Motion>')

def task(event):

    canv.delete('lines')
    x = event.x
    y = event.y

    for b in balls:
        canv.create_line(x,y,b.x,b.y, fill = 'orange', width = 2, tag = 'lines')

                canv.bind('<Button-1>', fill_random)
canv.bind('<Button-3>',line_on)

mainloop()

Вот так легко в Python можно менять реакцию на события.

Для того, чтобы находить шарики, находящиеся в определенной точке, можно использовать как «математический» метод и анализировать координаты, а можно воспользоваться методом find_overlapping.
Рассмотрим оба варианта:

def task(event):
    x = event.x
    y = event.y

    for b in balls:
        if math.sqrt((b.x - x)**2 + (b.y - y)**2) < b.r:
            canv.itemconfig (b.id, fill = 'orange')
canv.bind('<Button-1>', fill_random)
canv.bind('<Button-3>',task)

mainloop()

Корень находится в модуле math, который нужно подключить. Добавьте в начало программы

import math
def task(event):
    x = event.x
    y = event.y
    for b_id in canv.find_overlapping(x,y,x,y):
        canv.itemconfig (b_id, fill = 'orange')

Способ более простой, на первый взгляд, но его проблема в том, что мы получаем не объект, а всего лишь его id на экране. Это не всегда удобно.
Чтобы найти объект, чей рисунок на экране имеет код b_id, можно сделать так:

def task(event):
    x = event.x
    y = event.y
    for b_id in canv.find_overlapping(x,y,x,y):
        b = [b for b in balls if b.id == b_id][0]
        canv.itemconfig (b.id, fill = 'orange')
        

b = [b for b in balls if b.id == b_id] — дает список шариков, чьи рисунки имеют код b_id. Понятно, что такой шарик будет один, но мы-то получаем список! [0] позволяет взять первый элемент списка (единственный). Если этого не сделать, то b.id будет возращать ошибку

Рассмотрим, как можно добавлять и удалять элементы. В Python нет ничего сложного в добавлении и удалении элементов списка, вопрос в том, как прорисовать шарики после добавления.
В предыдущих уроках мы разбирали, различия между числовыми списками, и списками, заполненными объектами. Я показал, что изменяя размер шарика, мы не изменяем элемент списка, потому что в списке храняться объекты. В данном случае мы можем воспользоваться похожим приемом. Вместо объявления balls как глобальной переменной будем пользоваться методом append. Т.е. вместо:

def task(event):
    global balls
    balls += [ball()]

напишем так:

def task(event):
    x = event.x
    y = event.y
    balls.append(ball())
    balls[-1].paint()

Следует отметить, что отсутсвие директивы global не означает, что глобальная переменная не была использована. Это означает лишь то, что мы ее формально не изменяли.

Аналогичный метод можно использовать для вставки в случайное место:

def task(event):
    x = event.x
    y = event.y
    n = rnd(len(balls))
    balls.insert(n,ball())
    balls[-1].paint()

На вид нет особой разницы. Сейчас мы соединим линиями все шарики в том порядке, в каком они лежат в списке и убедимся, что вставили шарик не в конец списка, а в его середину (в случайное место).

def task(event):
    x = event.x
    y = event.y
    n = rnd(len(balls))
    balls.insert(n,ball())
    balls[-1].paint()
    canv.delete('lines')
    b_prev = balls[0]
    for b in balls[1:]:
        canv.create_line(b.x,b.y,b_prev.x,b_prev.y, fill = 'orange', tag = 'lines')
        b_prev = b

Во многих случаях нужно находить ближайший объект. Попробуем найти шарики, ближайший к щелчку мыши. Как это часто бывает, решить данную задачу можно двумя способами: с помощью анализа координат, и с помощью find_closest:

def task(event):
    x = event.x
    y = event.y
    for b_id in canv.find_closest(x,y):
        canv.itemconfig(b_id, fill = 'orange')

Тот, который ближе, будем сдвигать вправо:

def task(event):
    x = event.x
    y = event.y
    bm = balls[0]
    for b in balls[1:]:
        if ((b.x-x)**2 + (b.y - y)**2) < ((bm.x - x)**2 + (bm.y - y)**2):
            bm = b
    bm.x += 10
    bm.paint()
1. Почему в данном случае мы обошлись без использования квадратного кореня?

Изменим последнюю задачу так, чтобы это было забавнее, пусть шарик двигается не вправо, а «убегает от мыши». Для этого нужно найти направление вектора от мыши к шарику и сдвинуть шарик немного по этому направлению:

def task(event):
    x = event.x
    y = event.y
    bm = balls[0]
    for b in balls[1:]:
        if ((b.x-x)**2 + (b.y - y)**2) < ((bm.x - x)**2 + (bm.y - y)**2):
            bm = b
    bm.x += (bm.x - x) / 12
    bm.y += (bm.y - y) / 12
    bm.paint()

canv.bind('<Button-1>', fill_random)
canv.bind('<Motion>',task)

Как обычно, добавим включение и выключение «режима распугивания»:

def p_on(event):
    canv.bind('<Button-3>',p_off)
    canv.bind('<Motion>',task)

def p_off(event):
    canv.bind('<Button-3>',p_on)
    canv.unbind('<Motion>')

def task(event):
    x = event.x
    y = event.y
    bm = balls[0]
    for b in balls[1:]:
        if ((b.x-x)**2 + (b.y - y)**2) < ((bm.x - x)**2 + (bm.y - y)**2):
            bm = b
    bm.x += (bm.x - x) / 18
    bm.y += (bm.y - y) / 18
    bm.paint()

canv.bind('<Button-1>', fill_random)
canv.bind('<Button-3>',p_on)

mainloop()
2. Как сделать, чтобы шарики не убегали от мыши, а наоборот — бежали к ней?

Найти геометрический центр группы — это значит найти среднее арифметическое координат шариков по оси X и по оси Y. Отметим найденный центр линиями.

def task(event):
    x = event.x
    y = event.y
    sx = 0
    sy = 0
    for b in balls:
        sx += b.x
        sy += b.y
    sx = sx / len(balls)
    sy = sy / len(balls)
    canv.create_line(sx,0,sx,600, tag = 'lines')
    canv.create_line(0,sy,800,sy, tag = 'lines')

canv.bind('<Button-1>', fill_random)
canv.bind('<Button-3>',task)

mainloop()

А теперь выберем центр только красных:

def task(event):
    x = event.x
    y = event.y
    sx = 0
    sy = 0
    k = 0
    for b in balls:
        if b.color == 'red':
            sx += b.x
            sy += b.y
            k += 1
    sx = sx / k
    sy = sy / k
    canv.create_line(sx,0,sx,600, tag = 'lines')
    canv.create_line(0,sy,800,sy, tag = 'lines')

В прошлых уроках мы уже использовали анимацию, будем использовать ее и сейчас, сделаем плавно расширяющийся круг:

def task(event):
    canv.delete('temp')
    x = event.x
    y = event.y
    r = 0
    tt = canv.create_oval(x-r,y-r,x+r,y+r, fill = '#cc88ff', tag = 'temp')
    while r < 40:
        r += 5
        canv.coords(tt,x-r,y-r,x+r,y+r)
        canv.update()
        time.sleep(0.01)

В начале программы добавьте import time для подключения модуля time, иначе получите ошибку при попытке использовать time.sleep.
Если забудете написать canv.update(), то будете смотреть на черный экран, пока круг рисуется.

Усложним задачу и будем расширять круг, пока он не коснется какого-либо другого круга:

def task(event):
    canv.delete('temp')
    x = event.x
    y = event.y
    r = 0
    tt = canv.create_oval(x-r,y-r,x+r,y+r, fill = '#cc88ff', tag = 'temp')
    work = 1
    while work:
        canv.coords(tt,x-r,y-r,x+r,y+r)
        r += 3
        canv.update()
        for b in balls:
            if math.sqrt((b.x-x)**2+(b.y-y)**2) < (r+b.r):
                work = 0
                break

        time.sleep(0.01)

Иногда нужно определить, как объекты на экране пересекаются. Это, опять же, можно сделать двумя способами: через анализ координат и через методы canvas.find_overlapping и canvas.find_closest.

Рассмотрим только вариант с координатами. Для этого нужно либо перебрать все шарики вложенным циклом: для каждого шарика брать все остальные, либо придумать другой способ сформировать список пар для проверки — каждый со всеми остальными. Можно воспользоваться замечательным модулем itertools.
Посмотрим на простой пример:

    b = ['A','B','C','D']
    print(list(itertools.combinations(b,2)))

Мы получим все возможные сочетания без повторов, то что нужно.

Теперь придумаем, что делать с шариками, которые пересекаются. Я предлагаю их объединять — т.е. вместо двух шариков будет один, площадь которого равна сумме площадей пересекашихся шариков.

def task(event):
    for b1,b2 in itertools.combinations(balls,2):
        #print(b1.x,b2.x)
        if math.sqrt((b1.x - b2.x)**2 + (b1.y - b2.y)**2) < (b1.r + b2.r):
            new_b = ball()
            s = (math.pi * b1.r**2) + (math.pi * b2.r**2)
            new_b.r =  math.sqrt(s / math.pi)
            new_b.x =  (b1.x + b2.x) / 2
            new_b.y =  (b1.y + b2.y) / 2
            balls.append(new_b)
            canv.delete(b1.id)
            canv.delete(b2.id)
            balls.remove(b1)
            balls.remove(b2)

    for b in balls:
        b.paint()
3. Однако такой подход в некоторых случаях создает проблемы. В каких?
И что делать?

4. Все соприкасающиеся выделить красной границей
5. Все соприкасающиеся объединить. Площадь нового = сумме площадей. Цвет брать от самого большого
6. Красной рамкой отметить те, по которым щелкнули мышкой (если это точка пересечения, то выделить нужно все круги, которым принадлежит эта точка)
7. После щелчка на шарике, соединить его линиями со всеми остальными. Щелчок мимо шарика — игнорировать
8. Соединить линиями все шарики со всеми
9. Соединить линиями все красные шарики
10. Соединить линиями по шарики по порядку в списке
11. По щелчку мыши добавлять новые шарики в конец списка (проверить предыдущим заданием)
12. По щелчку мыши добавлять новые шарики в случайное место списка (проверить предыдущим заданием)
13. Провести линии от самого большого ко всем остальным (если есть равные самому большому, то выбрать любой)
14. Провести линии от самого большого ко всем остальным (если есть равные самому большому, то от всех)
15. Провести горизонтальную и вертикальную линии в геометрическом центре группы шариков
16. Крестиком соответствующего цвета отметить центр группы шариков каждого цвета
17*. Щелчок мышкой захватывает круг и начинает его перемещать. Еще один щелчок — отпускает круг.
18*. Щелчок мышкой по шарику «включает тяготение» для этого шарика и шарик падает (до нижней границы экрана)
19. Щелчок мышкой захватывает круг и начинает его перемещать. Еще один щелчок — отпускает круг. При перемещении рисовать линии от взятого шарика ко всем остальным. После освобождения шарика — линии удалять
20*. Щелчок мышкой включает «прицел» — линейку вертикальную и горизонтальную с насечками и цифрами расстояния, создает перемещающееся перекрестье. При наведении на любой шарик выводит информацию по нему на экран (текст поверх всех шариков). При уведении перекрестья с шарика — информацию удалять с экрана.
21. По щелчку мыши сделать оранжевым самый ближний к месту щелчка
22. По щелчку мыши в любом месте найти ближайший и провести к нему линию. Щелчок на шарике игнорировать
23. По щелчку включить включить режим указания ближайшего: при перемещении мыши проводить к нему линию. При повторном щелчке — выключить этот режим.
24. По щелчку включить режим указания двух ближайших (рисовать линию). Повторный щелчок выключает режим.
25. По щелчку начать рисовать круг в месте щелчка мыши, расширяя его, пока он не коснется одного из шариков
26*. По щелчку мышкой нарисовать линию (длиной 50), и начать вращать ее, пока она не коснется одного из шариков, подтянуть его к себе и плавно уменьшить его радиус, уничтожить его. Повторный щелчок должен выключать этот режим. Пока не выключен — линия перемещается вместе с мышью. Колесико удлиняет и укорачивает длину линии.
27**. Сделать предыдущее задание без задержки

One thought on “29. Списки и геометрия, забавные задачи

  1. Уведомление: Аноним

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *