Компьютерная графика → Рисование и формы в Java, Python и JS

Привет всем!

Сегодня расскажу про то, как рисовать и создавать формы используя разные языки как Java, Python и JS. Эта статья поможет вам понять только основы и покажет несколько примеров. Все остальное придется вам изучить самим. 

Мы напишем программу, в которую можно будет добавлять разные геометрические фигуры и их анимировать. 
Рисование и формы в Js, Python, JS

Итак, начнем с Python

Будем использовать библиотеку tkinter. В Windows она обычно идет вместе с Python, поэтому дополнительно ничего не надо устанавливать. Как проверить что она установлена? 
Запустите python интерпретатор в Командной строке и введите:
если у вас Python 3:

import tkinter

если у вас Python 2.7:

import Tkinter

Если вышла ошибка: ImportError: No module named Tkinter, значит tkinter у вас не установлен. Погуглите и найдите как его установить. 

И так, теперь можно начать писать программу. 

Сначала создадим файл и назовем его primitives_app.py: 

import Tkinter as tk  # Подключаем библиотеку и называем ее коротко tk

window = tk.Tk()  # Создаем новое окно
window.geometry("400x240")  # Задаем окну размеры
window.title(u"Primitive shapes")  # Задаем заголовок окна

.. Весь остальной код, будем добавлять сюда

window.mainloop()  # Отображаем окно

Мы создали простое окно. Запустите код и убедитесь что окно создано.

Теория:

Наверное вы помните, что когда вы пишете консольные приложения, после того выполнения доходит до последней строчки, программа останавливается? Когда мы работает с Окнами, мы запускаем бесконечный цикл ожидания. Метод mainloop() в конце запускает такой цикл. В этом цикле программа в каждой итерации проверяет не нажал ли пользователь что-то, не изменил ли окно и т.п. И если пользователь что-то делает программа отвечает запрограммированным образом. Когда окно закрывается, этот цикл завершается и программа останавливается. 

Мы создали окно. Теперь нам нужно сверху добавить панель для рисования, а снизу добавить элементы управления(кнопки и т.п.).

В tkinter все элементы, которые можно отображать на экране, называются Виджетами. Первый виджет, который мы добавим в наше окно будет Canvas (Полотно). В нем можно рисовать примитивные фигуры. При создании виджета мы должны указывать, внутри чего этот виджет будет расположен и его размеры. 

canvas = tk.Canvas(window, width=400, height=200)  # Создаем Canvas внутри window
canvas.pack()  # Размещаем его по порядку сверху вниз

Метод pack() - размещает элементы по порядку в родительском контейнере. Этот код добавим перед последней строчкой: window.mainloop().

Если сейчас запустим программу, мы Canvas не увидим, потому что он пока пустой, на нем ничего не нарисовано. Поэтому добавьте эту строчку, чтобы увидеть прямоугольник:

canvas.create_rectangle(35, 20, 140, 80, fill="#FF00FF")

Запустите программу, убедитесь что увидели прямоугольник и удалите эту строчку обратно. Подробно про рисование в Canvas можете прочитать здесь: Рисование в Tkinter.

В Canvas система координат перевернутая, в верхнем левом углу точка 0 - начало отсчета. Координата Y увеличивается вниз. Координата X как обычно увеличивается вправо. 

Рисование и формы в Js, Python, JS

Вот так работает Canvas. Идем дальше.

Теперь нам нужно добавить элементы управления. Там у нас 3 элемента: Выпадаещее меню, Кнопка "Add" и кнопка "Animate". Они должны быть у нас расположены горизонтально. Для этого мы добавим Frame и поместим все эти три внутри него. Frame - это виджет, который является контейнером и позволяет группировать элементы. 

frame = tk.Frame(window, width=400, height=40)  # Создаем Frame внутри window
frame.pack() # Размещаем его по порядку сверху вниз

Внутри этого фрейма сначала добавим выпадающее меню с двумя вариантами: "Rectangle" и "Circle". Виджет OptionMenu реализует Выпадающее меню. Он умеет показывать множество вариантов в списке. OptionMenu работает вместе с объектов StringVar, который умеет хранить тектовое значение. При изменении OptionMenu, будет изменяться значение связанного с ним объекта StringVar. И через этот объект StringVar мы будем получать выбранные вариант из выпадающего меню. 

Создаем сначала StringVar:

shapes_text = tk.StringVar()
shapes_text.set("Rectangle")  # Начальное значение "Rectangle"

Потом создаем OptionMenu и связываем его с созданным shapes_text:

option = tk.OptionMenu(frame, shapes_text, "Rectangle", "Circle")  # Добавляем внутри frame
option.pack(side=tk.LEFT)  # Размещаем по порядку слева

Сразу добавим две кнопки после них:

button = tk.Button(frame, text='Add', width=10)
button.pack(side=tk.LEFT)

button2 = tk.Button(frame, text='Animate, width=10)
button2.pack(side=tk.LEFT)

Запустите программу и вы увидите панель управления нашей программы. Теперь укажем, что надо делать при клике на эти кнопки. При создании объекта Button  можно указать параметр command и указать какую функцию надо вызвать при клике. Давайте перед созданием первой кнопки мы добавим метод add_new_shape_click() и укажем его первой кнопке как комманду:

def add_new_shape_click():
    print(shapes_text.get())

button = tk.Button(frame, text='Add', width=10, command=add_new_shape_click)
button.pack(side=tk.LEFT)

Теперь при нажатии на кнопку 'Add' в консоли будет печататься текущий выбранный вариант из Выпадающего меню. Теперь добавим команду для второй кнопки. При нажатии на кнопку "Animate" мы просто будем менять его текст на "Stop" и обратно на "Animate". Чтобы менять текст кнопки нам нужно создать еще один объект StringVar, для кнопки. Через этот объект мы будем менять текст кнопки и проверять текст кнопки. Теперь перед созданием второй кнопки нужно добавить:

animate_btn_text = tk.StringVar()
animate_btn_text.set("Animate")


def on_animate_click():
    if animate_btn_text.get() == 'Stop':
        animate_btn_text.set("Animate")
    else:
        animate_btn_text.set("Stop")


button2 = tk.Button(frame, textvariable=animate_btn_text, width=10, command=on_animate_click)
button2.pack(side=tk.LEFT)

При клике на вторую кнопку вызывается метод on_animate_click(), который проверяет текст кнопки и меняет его. Запускаем программку и проверяем.

Рисование 

Теперь пора начать рисовать. Создаем второй файл рядом с первым и называем drawer.py. В этом файле мы создадим несколько классов, который нам помогут рисовать. У нас будет два класса для фигур Rect для прямоугольников, Circle для кругов. В tkinter и те и другие рисуются с указанием координат двух углов с противоволожных сторон (левого-верхнего и нижнего-правого). Поэтому у этих фигур есть одинаковые свойства x,y - их координата и width, height - ширина и высота. Также кроме этого у каждой фигуры будет свой цвет(color). Так как и у прямоугольника и у круга есть одинаковые свойства, мы можем создать одного класса-родителя с этими свойства, и сделать классы для прямоугольника и круга потомками этого класса, чтобы они унаследовали все его свойства. Круто, да? Это называется Наследование в ООП. Класс-родитель мы назоваем Shape - фигура с английского. Давайте определим эти три класса:

Мы будем здесь использоваеть генератор случайных чисел, поэтому сразу его импортируем.  

Файл drawer.py:

from random import randint


class Shape(object):
    def __init__(self):
        self.x = 0
        self.y = 0
        self.width = 0
        self.height = 0
        self.color = "red"


class Rect(Shape):
    pass


class Circle(Shape):
    pass

Свойства класса указываются внутри метода __init__ через self. Все параметры у нас по умолчанию равны 0 и цвет красный. Классы Rect и Circle сейчас дополнительно ничего не добавляют к родительском свойствам и поэтому ничего пока от него не отличаются. Теперь добавим метод random_fill() к классу Shape, который будет случайным образом задавать параметры фигуре и задавать случайный цвет. Случайный цвет будет генерировать функуия random_color() который будет выбирать один цвет из списка:

def random_color():
    colors = ["#00ff00", "#00ffff", "#0099ff", "#6600ff", "#cc33ff", "#ff33cc", "#ff5050", "#ff9933", "#ffff00",
              "#99ff33", "#66ccff", "#990033"]
    return colors[randint(0, len(colors)-1)]


class Shape(object):
    def __init__(self):
        self.x = 0
        self.y = 0
        self.width = 0
        self.height = 0
        self.color = "red"

    def random_fill(self):
        self.x = randint(20, 380)
        self.y = randint(20, 180)
        self.width = randint(10, 50)
        self.height = randint(10, 50)
        self.color = random_color()

Посмотрите внимательно как генерируются случайные значения. Ширина и высота фигур от 10 до 50 пикселей. Давайте сделаем так, чтобы у кругов ширина и высота были одинаковыми, иначе круг не очень красивым станет. Для этого мы переопределим метод random_fill() в классе Circle, который унаследовал этот метод у своего родителя Shape. Добавим в класс Circle метод random_fill() и внутри него вызовем родительский rando_fill() и высоту приравняем к ширине и они будут равны. Так же? Такая штука называется Полиморфизмом в ООП.

class Circle(Shape):

    def random_fill(self):
        super(Circle, self).random_fill()
        self.height = self.width

Теперь пришла очередь создать Класс Drawer - который будет знать что и как рисовать. Этот класс мы будем использовать вместе с Canvas, и в этом классе мы будем собирать фигурки разные. Класс будет иметь методы add_rectangle() и add_circle() которые будут добавлять в набор прямоугольники и круги соответственно. 

class Drawer:
    def __init__(self, canvas):
        self.canvas = canvas
        self.shapes = []  # в этом списке будем собирать фигурки

    def add_rectangle(self):
        rect = Rect()       # создаем объект класса Rect
        rect.random_fill()  # заполняем
        self.shapes.append(rect) # добавляем в список фигур

    def add_circle(self):
        circle = Circle()
        circle.random_fill()
        self.shapes.append(circle)

Теперь к этому классу мы добавим метод redraw() который будет очищать Canvas и рисовать туда все фигуры из своего списка. Чтобы рисовать фигурки мы будем вызывать метод draw() и самих объектов фигур: 

class Drawer:
    ...

    def redraw(self):
        self.canvas.delete("all")  # удаляем все что до этого нарисовано
        for shape in self.shapes:   # переходим по всем фигурам
            shape.draw(self.canvas)  # и вызываем у них метод draw и передаем canvas как параметр

У класса фигур теперь должен быть метод draw() который принимает объект Canvas и рисует туда себя. Добавим в класс Shape метод draw():

class Shape(object):
    ...

    def draw(self, canvas):
        pass

Так, как класс Shape сам по себе не представляет никакой фигуры, она ничего не будет рисовать. ключевое слов pass в питоне означает - ничего не делать и идти дальше. А рисовать будут его потомки Rect и Circle. И что мы здесь будем делать? Правильно! Переопределим метод draw() в потомках класса Shape, чтобы каждый рисовал свою фигуру в canvas:

class Rect(Shape):

    def draw(self, canvas):
        canvas.create_rectangle(self.x, self.y, self.x+self.width, self.y+self.height, fill=self.color)


class Circle(Shape):

    def draw(self, canvas):
        canvas.create_oval(self.x, self.y, self.x+self.width, self.y+self.height, fill=self.color)

    def random_fill(self):
        super(Circle, self).random_fill()
        self.height = self.width

У нас все готово для рисования. Теперь мы должны добавить класс Drawer в предыдущий файл и использовать его там. Откроем файл primitives_app.py и сделаем импорт класса Drawer:

from drawer import Drawer

Потом создадим объект класса Drawer. Так как при создании объекта класса Drawer мы должны указать Canvas. Мы создаем объект Drawer после создания Canvas:

canvas = tk.Canvas(window, width=400, height=200)  # Создаем Canvas внутри window
canvas.pack()  # Размещаем его по порядку сверху вниз

drawer = Drawer(canvas)

и сделаем так чтобы при клике на кнопку "Add" в drawer добавлялась выбранная в списке фигура. изменим метод add_new_shape_click():

def add_new_shape_click():
    if shapes_text.get() == 'Rectangle':
        drawer.add_rectangle()
    elif shapes_text.get() == 'Circle':
        drawer.add_circle()
    drawer.redraw()

После добавления каждой фигуры, перерисовываем в Drawer. Попробуйте запустить программу и добавить фигуры. 

Анимация

Фигуры теперь у нас есть. Осталось добавить анимацию простую. Фигурки будут у нас двигаться вверх вниз. 

Как вы знаете анимация это когда много кадров быстро заменяются, а не какая-то магия ) Точно так же мы будем каждые несколько секунд координаты наших фигур на несколько пикселей и перерисовывать их. И будет казаться что они двигаются непрерывно. 

Мы будем двигать фигурки вверх пока они их координата Y не дойдет до 0, потом будем двигать вниз пока их координата Y не станет больше 180. Все вроде просто, но есть одна загвоздка. Допустим если фигура находится в точке Y=100, куда ее двигать вверх или вниз? А если в точке Y=10 ? Подумайте, перед там как дальше читать. Ок? .................... ................... У каждой фигуры будет направление или скорость. Дополнительное свойство. Мы назовем его speed. Это будет целое число, положительное или отрицательное и будет показывать на сколько пикселей будет сдвинута фигура за один такт. Если значение отрицательное, то координата Y будет уменьшаться, и соответственно обратно если значение положительное. 

Добавляем в класс Shape свойство speed, по умолчанию +2:

class Shape(object):
    def __init__(self):
        self.x = 0
        self.y = 0
        self.width = 0
        self.height = 0
        self.color = "red"
        self.speed = +2

В класс Drawer добавляем метод next_state() который один раз двигает все фигуры и перерисовывает их:

class Drawer:
    ...
    ...

    def next_state(self):
        for shape in self.shapes:        # Проходимся по всем фигурам
            shape.y += shape.speed       # меняем положение фигуры в соответствие со скоростью фигуры
            if shape.y <= 0 or shape.y >= 180:   # если фигура дошла верхнего или нижнего границ
                shape.speed = -shape.speed       # меняем скорость на противоположную
        self.redraw()        

Кстати, а еще чтобы мы могла останавливать анимацию если хотим, добавим свойство animating в класс Drawer. Мы будем анимировать только если это свойство True.:

class Drawer:
    def __init__(self, canvas):
        self.canvas = canvas
        self.shapes = []
        self.animating = False

 Теперь осталось только запустить анимацию по кнопке "Animate". Для этого поменяем тело метода on_animate_click(), чтобы он запускал и останавливал анимацию:

def on_animate_click():
    drawer.animating = not drawer.animating   # меняем свойство animating на обратную
    if drawer.animating:                  # меняем текст кнопки
        animate_btn_text.set("Stop")
    else:
        animate_btn_text.set("Animate")

    animate()   # запускаем метод для анимирования

Это в файле primitives_app.py. Осталось добавить один метод animate() который будет будет двигать фигуры на следующее состояние:

def animate():
    if drawer.animating:           # если анимация включена
        drawer.next_state()        # меняем состония фигур на следующее
        window.after(10, animate)  # ставим таймер на 10 мс чтобы снова вызвать метод animate

Все готово. Задание успешно выполнено! Полный код можете посмотреть здесь: Gist Github 

Теперь то же самое в Java

Подробно описывать не стал. Вот код https://gist.github.com/MasterAlish/0efc33ce4a20b449aac43ea37b85e5ef. Запускаем файл PrimitivesApp.java.

Чтобы понять можете прочитать две статьи, где все подробно написано:
1. Работа с формами и рисование(график)
2. Анимация и двойная буферизация при рисовании

И наконец JavaScript:

Вы все знаете html, js, jquery. Думаю понять код будет не сложно https://gist.github.com/MasterAlish/e98ded685bf2437bf63eea23fe99303a 

Удачи!

1812 0
Alisher Alikulov