Skip to content

Commit 7c8bda3

Browse files
committed
transfer
1 parent b9cb266 commit 7c8bda3

File tree

2 files changed

+337
-0
lines changed

2 files changed

+337
-0
lines changed

__main__.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import tkinter as tk
2+
import tkinter.ttk as ttk
3+
import typing
4+
from functools import lru_cache
5+
from random import random
6+
from tkinter.colorchooser import askcolor
7+
from tkinter.filedialog import asksaveasfilename
8+
9+
import glm
10+
from PIL import Image, ImageTk, ImageDraw
11+
12+
# Имена доступных секций
13+
# Available section names
14+
SECTIONS = 'first', 'second', 'third', 'fourth'
15+
16+
# Стандандартные значения всех параметров
17+
# Standard values of all parameters
18+
DEFAULTS = {
19+
'first': {'form': glm.mat2(0, 0, 0, .16), 'offset': glm.vec2(0, 0), 'probability': .01},
20+
'second': {'form': glm.mat2(.85, .04, -.04, .85), 'offset': glm.vec2(0, 1.6), 'probability': .86},
21+
'third': {'form': glm.mat2(-0.2, .26, .23, .4), 'offset': glm.vec2(0, 1.6), 'probability': .93},
22+
'fourth': {'form': glm.mat2(-.15, .28, .26, .24), 'offset': glm.vec2(0, .44), 'probability': 0},
23+
'number of points': 10 ** 5
24+
}
25+
26+
# Диапазоны для шкал (в этих диапазонах картинка выглядит прилично,
27+
# но пользователь в поле ввода может установить произвольное)
28+
# Ranges for scales (in these ranges the picture looks decent,
29+
# but the user can set arbitrary in the input field)
30+
SCALE_RANGES = {
31+
'first': {'form': (((-.5, .5), (-.5, .5)), ((-.07, .045), (-.5, .5))),
32+
'offset': ((-1, 1), (-5, 5)), 'probability': (0, 1)},
33+
34+
'second': {'form': (((.45, 1), (-.5, 1.5)), ((-.3, .2), (.3, .95))),
35+
'offset': ((-3, 1), (.2, 4)), 'probability': (0, 1)},
36+
37+
'third': {'form': (((-.5, .5), (-2, 2)), ((-1.5, 2), (-.28, .9))),
38+
'offset': ((-5, 5), (-4, 4)), 'probability': (0, 1)},
39+
40+
'fourth': {'form': (((-.8, .7), (-1, 2)), ((-1.5, 1.5), (-.5, .8))),
41+
'offset': ((-5, 5), (-7, 9)), 'probability': (0, 1)},
42+
43+
'number of points': (0, 2 * 10 ** 5)
44+
}
45+
46+
47+
@lru_cache(maxsize=1024)
48+
def __transform(form: glm.mat2, current_point: glm.vec2, offset: glm.vec2) -> glm.vec2:
49+
return form * current_point + offset
50+
51+
52+
def fern_point_generator(number_of_points: int, options: dict):
53+
"""
54+
Генерирует значения точек-пикселей папоротника
55+
56+
Generates fern pixel point values
57+
58+
Args:
59+
number_of_points (int): количество точек
60+
options (dict): собственные опции для точек, вместо стандартных
61+
own options for points instead of standard
62+
63+
Yields:
64+
glm.vec2: точка. A point
65+
"""
66+
current_point = glm.vec2(0, 0)
67+
68+
for point_no in range(number_of_points):
69+
value = random()
70+
71+
if value < options['first']['probability']:
72+
current_point = __transform(options['first']['form'], current_point, options['first']['offset'])
73+
elif value < options['second']['probability']:
74+
current_point = __transform(options['second']['form'], current_point, options['second']['offset'])
75+
elif value < options['third']['probability']:
76+
current_point = __transform(options['third']['form'], current_point, options['third']['offset'])
77+
else:
78+
current_point = __transform(options['fourth']['form'], -current_point, options['fourth']['offset'])
79+
80+
yield current_point
81+
82+
83+
def rescale_points(points: typing.Iterable, image_size: typing.Tuple[int, int]):
84+
"""
85+
Масштабирует точки (иначе размер папоротника очень мал и вверх ногами)
86+
87+
Scales points (otherwise the fern size is very small and upside down)
88+
89+
Args:
90+
points (typing.Iterable[glm.vec2]): Набор точек. Iterable of points.
91+
image_size (typing.Tuple[int, int]): Размер итогового изображения. Final image size
92+
93+
Yields:
94+
glm.vec2: Scaled point
95+
"""
96+
factor = min(image_size) / 10.5
97+
for point in points:
98+
point = point * factor
99+
yield int(point[0] + image_size[0] / 2), int(point[1])
100+
101+
102+
def create_scale_and_entry_pair(root, var: tk.Variable,
103+
scale_range: typing.Tuple[float, float],
104+
state='normal') -> tk.Frame:
105+
"""
106+
Создает пару: шкала - текстовое поле
107+
108+
Creates a pair: Scale - Entry
109+
110+
Args:
111+
root: родительский виджет для будущей пары. root widget for pair
112+
var (tk.Variable): переменная, с которой будет синхронизирована шкала и текстовое поле. the variable with which
113+
the scale and text box will be synchronized
114+
scale_range (typing.Tuple[float, float]): диапазон значений шкалы.
115+
state (str): состояние шкалы и текстового поля.
116+
117+
Returns:
118+
tk.Frame: готовая пара.
119+
"""
120+
frame = tk.Frame(root)
121+
122+
scale = ttk.Scale(frame, variable=var, from_=scale_range[0], to=scale_range[1])
123+
scale['state'] = state
124+
scale.pack(side=tk.LEFT)
125+
126+
entry = ttk.Entry(frame, textvar=var, width=8)
127+
entry['state'] = state
128+
entry.pack(side=tk.RIGHT)
129+
130+
return frame
131+
132+
133+
def rgb_byte_to_hex(color: typing.Tuple[int, ...]):
134+
"""
135+
Конвертирует RGB цвет в байтах (0..256) в HTML Hex
136+
137+
Converts RGB color in bytes (0..256) to HTML Hex
138+
139+
Args:
140+
color (typing.Tuple[int, ...]): цвет RGB. RGB color
141+
142+
Returns:
143+
str: HTML Hex
144+
"""
145+
return '#%.2x%.2x%.2x' % (color[0], color[1], color[2])
146+
147+
148+
class App:
149+
size = 720, 1024 # примерный размер окна в пикселях. approximate window size in pixels
150+
151+
main_color = (0, 255, 0) # цвет рисунка. picture color
152+
canvas_bg = (0, 0, 0) # цвет холста. canvas color
153+
154+
tk_vars = {}
155+
156+
image = None
157+
tk_image = None
158+
159+
allow_update = False
160+
161+
def __init__(self):
162+
self.root = tk.Tk()
163+
164+
self.__load_tk_vars()
165+
166+
self.canvas = tk.Canvas(self.root, width=self.size[0], height=self.size[0], bg=rgb_byte_to_hex(self.canvas_bg))
167+
self.canvas.grid(row=0, column=0, rowspan=2)
168+
169+
self.main_controls = tk.Frame(self.root)
170+
self.main_controls.grid(row=0, column=1)
171+
self.__load_main_controls()
172+
173+
self.additional_control = tk.Frame(self.root)
174+
self.additional_control.grid(row=1, column=1)
175+
self.__load_additional_controls()
176+
177+
self.__load_image()
178+
self.update()
179+
180+
def __load_tk_vars(self):
181+
self.tk_vars = {
182+
section: {
183+
'form': [
184+
[self.__gen_tk_var(DEFAULTS[section]['form'][0][0]),
185+
self.__gen_tk_var(DEFAULTS[section]['form'][0][1])],
186+
[self.__gen_tk_var(DEFAULTS[section]['form'][1][0]),
187+
self.__gen_tk_var(DEFAULTS[section]['form'][1][1])],
188+
],
189+
'offset': [self.__gen_tk_var(DEFAULTS[section]['offset'][0]),
190+
self.__gen_tk_var(DEFAULTS[section]['offset'][1])],
191+
'probability': self.__gen_tk_var(DEFAULTS[section]['probability'])
192+
}
193+
for section in SECTIONS
194+
}
195+
self.tk_vars['number of points'] = self.__gen_tk_var(DEFAULTS['number of points'])
196+
self.allow_update = True
197+
198+
def __load_additional_controls(self):
199+
num_of_points_frame = tk.Frame(self.additional_control)
200+
num_of_points_frame.pack()
201+
tk.Label(num_of_points_frame, text='Количество точек:', font=('Arial', 14)).pack(side=tk.LEFT, padx=5)
202+
ttk.Scale(num_of_points_frame, len_=self.size[0] / 2, variable=self.tk_vars['number of points'],
203+
from_=SCALE_RANGES['number of points'][0],
204+
to=SCALE_RANGES['number of points'][1]).pack(side=tk.LEFT)
205+
ttk.Entry(num_of_points_frame, textvar=self.tk_vars['number of points']).pack(side=tk.LEFT)
206+
207+
buttons_frame = tk.Frame(self.additional_control)
208+
buttons_frame.pack()
209+
210+
ttk.Button(buttons_frame, text='Обновить',
211+
command=self.update).pack(side=tk.LEFT)
212+
ttk.Button(buttons_frame, text='Восстановить стандартные значения',
213+
command=self.set_defaults).pack(side=tk.LEFT)
214+
ttk.Button(buttons_frame, text='Цвет заднего фона',
215+
command=self.change_bg).pack(side=tk.LEFT)
216+
ttk.Button(buttons_frame, text='Цвет рисунка',
217+
command=self.change_fg).pack(side=tk.LEFT)
218+
ttk.Button(buttons_frame, text='Сохранить изображение',
219+
command=self.save_image).pack(side=tk.LEFT)
220+
221+
def __gen_tk_var(self, value: float = None):
222+
var = tk.DoubleVar()
223+
if value is not None:
224+
var.set(value)
225+
var.trace_add('write', self.update)
226+
return var
227+
228+
def __form_controls(self, root, section):
229+
frame = tk.Frame(root)
230+
for i in range(2):
231+
for j in range(2):
232+
create_scale_and_entry_pair(frame, self.tk_vars[section]['form'][i][j],
233+
SCALE_RANGES[section]['form'][i][j]).grid(row=i, column=j)
234+
return frame
235+
236+
def __offset_controls(self, root, section):
237+
frame = tk.Frame(root)
238+
create_scale_and_entry_pair(frame, self.tk_vars[section]['offset'][0],
239+
SCALE_RANGES[section]['offset'][0]).pack(side=tk.LEFT)
240+
create_scale_and_entry_pair(frame, self.tk_vars[section]['offset'][1],
241+
SCALE_RANGES[section]['offset'][1]).pack(side=tk.RIGHT)
242+
return frame
243+
244+
def __probability_controls(self, root, section):
245+
return create_scale_and_entry_pair(root, self.tk_vars[section]['probability'],
246+
SCALE_RANGES[section]['probability'],
247+
state='normal' if section != 'fourth' else 'disabled')
248+
249+
def __section_controls(self, root, section):
250+
frame = tk.Frame(root)
251+
self.__form_controls(frame, section).pack(pady=10)
252+
self.__offset_controls(frame, section).pack(pady=10)
253+
self.__probability_controls(frame, section).pack(pady=10)
254+
return frame
255+
256+
def __load_main_controls(self):
257+
for no, section in enumerate(SECTIONS):
258+
self.__section_controls(self.main_controls, section).grid(row=no // 2, column=no % 2, pady=20, padx=20)
259+
260+
def __create_image(self):
261+
return Image.new('RGBA', (self.size[0], self.size[0]), color=self.canvas_bg)
262+
263+
def __load_image(self):
264+
self.image = self.__create_image()
265+
self.tk_image = ImageTk.PhotoImage(image=self.image)
266+
self.canvas.create_image(self.size[0] / 2 + 2, self.size[0] / 2 + 2, image=self.tk_image, tag=('image',))
267+
268+
def get_values(self):
269+
values = {
270+
section: {
271+
'form': glm.mat2x2(self.tk_vars[section]['form'][0][0].get(), self.tk_vars[section]['form'][0][1].get(),
272+
self.tk_vars[section]['form'][1][0].get(), self.tk_vars[section]['form'][1][1].get()
273+
),
274+
'offset': glm.vec2(self.tk_vars[section]['offset'][0].get(), self.tk_vars[section]['offset'][1].get()),
275+
'probability': self.tk_vars[section]['probability'].get()
276+
}
277+
for section in SECTIONS
278+
}
279+
return values
280+
281+
def update(self, *args):
282+
if self.image and self.allow_update:
283+
self.canvas.delete('image')
284+
self.image = self.__create_image()
285+
draw = ImageDraw.Draw(self.image)
286+
draw.point(list(set(rescale_points(fern_point_generator(int(self.tk_vars['number of points'].get()),
287+
self.get_values()), self.image.size))),
288+
fill=self.main_color)
289+
self.tk_image = ImageTk.PhotoImage(image=self.image.rotate(180))
290+
self.canvas.create_image(self.size[0] / 2 + 2, self.size[0] / 2 + 2, image=self.tk_image, tag=('image',))
291+
return args
292+
293+
def change_bg(self):
294+
color = tuple(map(int, askcolor(rgb_byte_to_hex(self.canvas_bg))[0]))
295+
self.canvas_bg = color
296+
self.canvas['bg'] = rgb_byte_to_hex(color)
297+
self.update()
298+
299+
def change_fg(self):
300+
color = tuple(map(int, askcolor(rgb_byte_to_hex(self.main_color))[0]))
301+
self.main_color = color
302+
self.update()
303+
304+
def save_image(self):
305+
path = asksaveasfilename()
306+
self.image.rotate(180).save(path + '.png', format='PNG')
307+
308+
def set_defaults(self):
309+
self.allow_update = False
310+
for section in SECTIONS:
311+
for i in range(2):
312+
for j in range(2):
313+
self.tk_vars[section]['form'][i][j].set(DEFAULTS[section]['form'][i][j])
314+
self.tk_vars[section]['offset'][0].set(DEFAULTS[section]['offset'][0])
315+
self.tk_vars[section]['offset'][1].set(DEFAULTS[section]['offset'][1])
316+
self.tk_vars[section]['probability'].set(DEFAULTS[section]['probability'])
317+
self.tk_vars['number of points'].set(DEFAULTS['number of points'])
318+
self.allow_update = True
319+
self.update()
320+
321+
def run(self):
322+
"""
323+
Запуск приложения
324+
Run this application
325+
326+
Returns:
327+
None
328+
"""
329+
self.root.mainloop()
330+
331+
332+
# Точка входа. Entry point
333+
if __name__ == '__main__':
334+
app = App()
335+
app.run()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Pillow ~= 8.0.1
2+
PyGLM ~= 1.99.3

0 commit comments

Comments
 (0)