by3axap commited on
Commit
8d08e78
·
verified ·
1 Parent(s): 7813956

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +213 -138
templates/index.html CHANGED
@@ -1,143 +1,218 @@
1
- <!DOCTYPE html>
2
- <html lang="ru">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>CELL CLIPPER PRO / MULTI-PATTERN</title>
7
- <style>
8
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=JetBrains+Mono&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- :root {
11
- --bg-page: #ffffff;
12
- --text-main: #000000;
13
- --text-muted: #aaaaaa;
14
- --border: #000000;
15
- --font-sans: 'Inter', sans-serif;
16
- --font-mono: 'JetBrains Mono', monospace;
 
 
 
 
17
 
18
- --color-pink: #FF0066;
19
- --color-lime: #CCFF00;
20
- --color-hover: #F0F0F0;
21
- --color-btn-gray: #E5E5E5;
22
- }
23
-
24
- * { box-sizing: border-box; margin: 0; padding: 0; }
25
-
26
- body {
27
- display: flex;
28
- height: 100vh;
29
- font-family: var(--font-sans);
30
- background-color: var(--bg-page);
31
- color: var(--text-main);
32
- overflow: hidden;
33
- }
34
-
35
- /* --- Sidebar --- */
36
- .sidebar {
37
- width: 380px;
38
- background: #ffffff;
39
- border-right: 1px solid var(--border);
40
- display: flex;
41
- flex-direction: column;
42
- padding: 20px;
43
- gap: 30px;
44
- z-index: 10;
45
- overflow-y: auto;
46
- flex-shrink: 0;
47
- }
48
-
49
- .section { display: flex; flex-direction: column; gap: 15px; }
50
 
51
- .section-title {
52
- font-family: var(--font-mono);
53
- font-size: 10px;
54
- color: var(--text-muted);
55
- text-transform: uppercase;
56
- letter-spacing: 0.1em;
57
- }
58
-
59
- /* --- Buttons --- */
60
- input[type="file"] { display: none; }
61
 
62
- .btn-upload {
63
- display: flex;
64
- justify-content: center;
65
- align-items: center;
66
- width: 100%;
67
- padding: 16px 20px;
68
- border: 1px solid var(--border);
69
- background: transparent;
70
- color: var(--text-main);
71
- font-family: var(--font-mono);
72
- font-size: 12px;
73
- cursor: pointer;
74
- transition: background 0.2s, border-color 0.2s;
75
- text-align: center;
76
- }
77
- .btn-upload:hover { background: var(--color-hover); }
78
-
79
- .btn-upload.active-pink { background: var(--color-pink); border-color: var(--color-pink); color: #000; }
80
- .btn-upload.active-lime { background: var(--color-lime); border-color: var(--color-lime); color: #000; }
81
-
82
- /* --- Pattern List --- */
83
- .patterns-list { display: flex; flex-direction: column; gap: 10px; }
84
- .pattern-item { display: flex; align-items: center; border: 1px solid var(--border); padding: 10px 15px; gap: 15px; background: #fff; }
85
- .pattern-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; }
86
- .pattern-icon img { max-width: 100%; max-height: 100%; }
87
- .pattern-name { flex: 1; font-family: var(--font-mono); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88
-
89
- .btn-remove { color: #FF0000; background: none; border: none; font-family: var(--font-mono); font-size: 10px; cursor: pointer; text-transform: uppercase; }
90
- .btn-remove:hover { text-decoration: underline; }
91
-
92
- /* --- Controls --- */
93
- .control-group { display: flex; flex-direction: column; gap: 10px; }
94
- .label-row { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--font-mono); font-size: 11px; }
95
- .val-display { font-family: var(--font-mono); font-size: 11px; }
96
-
97
- input[type="range"] { width: 100%; -webkit-appearance: none; background: transparent; cursor: pointer; }
98
- input[type="range"]::-webkit-slider-runnable-track { width: 100%; height: 2px; background: #000000; border: none; }
99
- input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; height: 16px; width: 4px; background: #000000; border-radius: 0; margin-top: -7px; border: none; }
100
- input[type="range"]:focus { outline: none; }
101
-
102
- /* --- Footer Controls --- */
103
- .footer-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
104
- .checkbox-label { display: flex; align-items: center; gap: 10px; font-family: var(--font-mono); font-size: 11px; cursor: pointer; }
105
- input[type="checkbox"] { accent-color: #000; width: 14px; height: 14px; cursor: pointer; }
106
-
107
- .color-pickers { display: flex; gap: 10px; }
108
- input[type="color"] { -webkit-appearance: none; appearance: none; border: none; width: 20px; height: 20px; padding: 0; background: none; cursor: pointer; }
109
- input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
110
- input[type="color"]::-webkit-color-swatch { border: 1px solid rgba(0,0,0,0.2); border-radius: 0; }
111
-
112
- /* --- Action Buttons --- */
113
- .action-buttons { display: flex; gap: 15px; margin-top: auto; }
114
- .btn-action { flex: 1; padding: 16px 0; font-family: var(--font-mono); font-size: 12px; border: none; cursor: pointer; transition: opacity 0.2s; text-align: center; }
115
- .btn-action:hover { opacity: 0.8; }
116
- .btn-download-svg { background: #000000; color: #ffffff; }
117
- .btn-download-mp4 { background: var(--color-btn-gray); color: #ffffff; }
118
- .btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
119
-
120
- /* --- Preview Area --- */
121
- .preview-area {
122
- flex: 1;
123
- background-color: #ffffff;
124
- display: flex;
125
- align-items: center;
126
- justify-content: center;
127
- padding: 20px;
128
- height: 100vh;
129
- overflow: hidden; /* Запрет скролла */
130
- }
131
 
132
- #main-canvas {
133
- box-shadow: 0 10px 40px rgba(0,0,0,0.05);
134
- max-width: 100%;
135
- max-height: 100%;
136
- object-fit: contain; /* Пропорциональное вписывание */
137
- background: #fff;
138
- }
139
-
140
- #source-video, #source-image { display: none; }
141
- </style>
142
- </head>
143
- <body>
 
1
+ import os
2
+ import math
3
+ import re
4
+ import tempfile
5
+ from flask import Flask, render_template, request, send_file, Response
6
+ from PIL import Image, ImageOps, ImageDraw
7
+ import numpy as np
8
+
9
+ # Проверка наличия MoviePy
10
+ try:
11
+ from moviepy.editor import VideoFileClip
12
+ MOVIEPY_AVAILABLE = True
13
+ except ImportError:
14
+ MOVIEPY_AVAILABLE = False
15
+
16
+ app = Flask(__name__)
17
+
18
+ # --- Вспомогательные функции ---
19
+
20
+ def prepare_svg_content(file_storage):
21
+ """Читает SVG и готовит его для вставки"""
22
+ try:
23
+ raw_data = file_storage.read().decode('utf-8')
24
+ vb_match = re.search(r'viewBox=["\']([^"\']+)["\']', raw_data, re.I)
25
+ vbox = vb_match.group(1) if vb_match else "0 0 100 100"
26
+
27
+ inner = re.sub(r'<svg[^>]*>', '', raw_data, flags=re.I)
28
+ inner = re.sub(r'</svg>', '', inner, flags=re.I)
29
+ inner = re.sub(r'<\?xml[^>]*\?>|<!DOCTYPE[^>]*>', '', inner, flags=re.I)
30
+
31
+ if 'fill=' not in inner:
32
+ inner = f'<g fill="black">{inner}</g>'
33
+ return inner, vbox
34
+ except:
35
+ return '<rect width="100" height="100" fill="black"/>', "0 0 100 100"
36
+
37
+ def calculate_grid_items(width, height, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_data=None):
38
+ """Математическое ядро расчета сетки"""
39
+ step = width / density
40
+ rows = math.ceil(height / step)
41
+
42
+ cell_w_max = step * (1.0 - gap_x)
43
+ cell_h_max = step * (1.0 - gap_y)
44
+
45
+ items = []
46
+
47
+ for r in range(rows):
48
+ row_shift = (r * shift * step)
49
+ # Оптимизация цикла по колонкам
50
+ start_c = -int(density * 0.5)
51
+ end_c = int(density * 1.5)
52
+
53
+ for c in range(start_c, end_c):
54
+ grid_x = c * step + row_shift
55
+ grid_y = r * step
56
+
57
+ if grid_x < -step or grid_x > width:
58
+ continue
59
+
60
+ # Логика яркости
61
+ is_visible = True
62
+
63
+ if pixel_data is not None:
64
+ # pixel_data должен быть PIL Image (Grayscale)
65
+ sx = max(0, min(int(grid_x + step/2), width - 1))
66
+ sy = max(0, min(int(grid_y + step/2), height - 1))
67
+ try:
68
+ val = pixel_data.getpixel((sx, sy))
69
+ brightness = val / 255.0
70
+ limit = 0.5
71
+ is_visible = brightness > limit if invert else brightness < limit
72
+ except:
73
+ pass
74
+
75
+ if not is_visible:
76
+ continue
77
+
78
+ # Логика искажения (Growth)
79
+ wave = 0
80
+ if growth_type == 'sine':
81
+ wave = (math.sin(c * 0.3 + r * 0.3) + 1) / 2
82
+ elif growth_type == 'linear':
83
+ wave = (c % density) / density
84
+ elif growth_type == 'steps':
85
+ wave = (r % 5) / 5.0
86
+
87
+ mod_w = 1.0 - (wave * growth)
88
+ mod_w = max(0.01, min(1.0, mod_w))
89
+
90
+ content_w = cell_w_max * base_scale * mod_w
91
+ content_h = cell_h_max * base_scale
92
+
93
+ cx = grid_x + step / 2
94
+ cy = grid_y + step / 2
95
+
96
+ items.append({
97
+ 'cx': cx, 'cy': cy,
98
+ 'w': content_w, 'h': content_h
99
+ })
100
+
101
+ return items, step
102
+
103
+ # --- Роуты ---
104
+
105
+ @app.route('/')
106
+ def index():
107
+ return render_template('index.html')
108
+
109
+ @app.route('/download_svg', methods=['POST'])
110
+ def download_svg():
111
+ try:
112
+ # Параметры
113
+ density = int(request.form.get('density', 40))
114
+ gap_x = float(request.form.get('gap_x', 0))
115
+ gap_y = float(request.form.get('gap_y', 0))
116
+ shift = float(request.form.get('shift_stagger', 0))
117
+ base_scale = float(request.form.get('base_scale', 1.0))
118
+ growth = float(request.form.get('growth', 0))
119
+ growth_type = request.form.get('growth_type', 'sine')
120
+ invert = request.form.get('invert') == 'true'
121
+
122
+ canvas_w, canvas_h = 1400, 900
123
+
124
+ # SVG Symbol
125
+ svg_file = request.files.get('shape_svg')
126
+ if svg_file and svg_file.filename != '':
127
+ symbol_content, v_box = prepare_svg_content(svg_file)
128
+ else:
129
+ symbol_content, v_box = ('<rect width="100" height="100" fill="black"/>', "0 0 100 100")
130
+
131
+ vb_parts = [float(x) for x in v_box.split()]
132
+ vb_w, vb_h = vb_parts[2], vb_parts[3]
133
+
134
+ # Raster Image для маски
135
+ raster_file = request.files.get('raster_image')
136
+ pixel_img = None
137
+ if raster_file and raster_file.filename != '':
138
+ img = Image.open(raster_file)
139
+ img = ImageOps.fit(img, (canvas_w, canvas_h), method=Image.Resampling.LANCZOS)
140
+ img = ImageOps.grayscale(img)
141
+ pixel_img = img
142
+
143
+ items, _ = calculate_grid_items(canvas_w, canvas_h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_img)
144
+
145
+ body = []
146
+ for item in items:
147
+ sx = item['w'] / vb_w
148
+ sy = item['h'] / vb_h
149
+ transform = f'translate({item["cx"]:.2f} {item["cy"]:.2f}) scale({sx:.4f} {sy:.4f}) translate({-vb_w/2:.2f} {-vb_h/2:.2f})'
150
+ body.append(f'<g transform="{transform}">{symbol_content}</g>')
151
+
152
+ svg_str = f'<svg xmlns="http://www.w3.org/2000/svg" width="{canvas_w}" height="{canvas_h}" viewBox="0 0 {canvas_w} {canvas_h}" style="background:white;">{"".join(body)}</svg>'
153
+
154
+ return Response(svg_str, mimetype="image/svg+xml", headers={"Content-disposition": "attachment; filename=pattern.svg"})
155
+
156
+ except Exception as e:
157
+ return str(e), 500
158
+
159
+ @app.route('/process_video', methods=['POST'])
160
+ def process_video():
161
+ if not MOVIEPY_AVAILABLE:
162
+ return "MoviePy not installed on server", 500
163
+
164
+ temp_dir = tempfile.mkdtemp()
165
+ try:
166
+ density = int(request.form.get('density', 40))
167
+ gap_x = float(request.form.get('gap_x', 0))
168
+ gap_y = float(request.form.get('gap_y', 0))
169
+ shift = float(request.form.get('shift_stagger', 0))
170
+ base_scale = float(request.form.get('base_scale', 1.0))
171
+ growth = float(request.form.get('growth', 0))
172
+ growth_type = request.form.get('growth_type', 'sine')
173
+ invert = request.form.get('invert') == 'true'
174
+
175
+ video_file = request.files.get('video_file')
176
+ if not video_file:
177
+ return "No video uploaded", 400
178
 
179
+ input_path = os.path.join(temp_dir, 'input.mp4')
180
+ video_file.save(input_path)
181
+
182
+ def process_frame(get_frame, t):
183
+ frame = get_frame(t)
184
+ img_pil = Image.fromarray(frame).convert("L")
185
+ w, h = img_pil.size
186
+
187
+ # Рендеринг кадра
188
+ canvas = Image.new("RGB", (w, h), "white")
189
+ draw = ImageDraw.Draw(canvas)
190
 
191
+ # Используем ту же логику расчета
192
+ items, _ = calculate_grid_items(w, h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, img_pil)
193
+
194
+ for item in items:
195
+ # Рисуем прямоугольники (быстро для видео)
196
+ # Если нужно SVG в видео, это сильно замедлит процесс
197
+ x0 = item['cx'] - item['w']/2
198
+ y0 = item['cy'] - item['h']/2
199
+ draw.rectangle([x0, y0, x0 + item['w'], y0 + item['h']], fill="black")
200
+
201
+ return np.array(canvas)
202
+
203
+ clip = VideoFileClip(input_path)
204
+ # Ограничение FPS для ускорения, если видео большое
205
+ # clip = clip.set_fps(24)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ new_clip = clip.fl(process_frame)
 
 
 
 
 
 
 
 
 
208
 
209
+ output_path = os.path.join(temp_dir, 'output.mp4')
210
+ new_clip.write_videofile(output_path, codec='libx264', audio_codec='aac', fps=clip.fps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ return send_file(output_path, as_attachment=True, download_name="processed_video.mp4")
213
+
214
+ except Exception as e:
215
+ return f"Error processing video: {str(e)}", 500
216
+
217
+ if __name__ == '__main__':
218
+ app.run(host='0.0.0.0', port=7860)