sterepando commited on
Commit
00d530e
·
verified ·
1 Parent(s): e8b10b3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -0
app.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ from PIL import Image
4
+ import io
5
+ import base64
6
+
7
+ # Встроенный HTML+JS+CSS (твой код, но чуть доработанный под Gradio)
8
+ html_content = """
9
+ <!DOCTYPE html>
10
+ <html lang="ru">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>Swaga Icon Maker</title>
15
+ <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;900&display=swap" rel="stylesheet">
16
+ <style>
17
+ * { font-family: 'Nunito', sans-serif; box-sizing: border-box; }
18
+ body { background:#1e1e1e; color:white; margin:0; padding:20px; display:flex; flex-direction:column; align-items:center; user-select:none; }
19
+ h1 { font-weight:900; margin-bottom:10px; }
20
+ .container { display:flex; gap:30px; flex-wrap:wrap; justify-content:center; max-width:100%; }
21
+ canvas { border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,0.5); background:#E13839; }
22
+ .controls { background:#2d2d2d; padding:20px; border-radius:15px; width:300px; display:flex; flex-direction:column; gap:15px; }
23
+ .section-title { font-size:12px; text-transform:uppercase; color:#777; letter-spacing:1px; border-bottom:1px solid #444; padding-bottom:5px; margin-top:10px; font-weight:900; }
24
+ .drop-zone { border:3px dashed #555; border-radius:10px; padding:20px; text-align:center; cursor:pointer; transition:0.3s; }
25
+ .drop-zone:hover, .drop-zone.dragover { border-color:#E13839; background:rgba(225,56,57,0.1); }
26
+ input[type="range"] { width:100%; accent-color:#E13839; }
27
+ button { border:none; padding:15px; border-radius:8px; font-weight:900; font-size:16px; cursor:pointer; transition:transform 0.1s; }
28
+ .btn-primary { background:linear-gradient(225deg,#FFF 0%,#FFACC7 100%); color:#E13839; }
29
+ .btn-secondary { background:#444; color:white; padding:10px; font-size:14px; }
30
+ button:active { transform:scale(0.98); }
31
+ .hint { font-size:12px; color:#666; text-align:center; margin-top:5px; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <h1>MandreIcon Creator</h1>
36
+ <div class="container">
37
+ <div style="display:flex; flex-direction:column; align-items:center;">
38
+ <canvas id="canvas" width="512" height="512"></canvas>
39
+ <p class="hint">Клик → выделить • Drag → двигать • Колесо/два пальца → вращать • Уголок → масштабировать • Delete → удалить</p>
40
+ </div>
41
+
42
+ <div class="controls">
43
+ <div class="section-title">Фон (Base Icon)</div>
44
+ <div class="drop-zone" id="dropBase">
45
+ <p><b>SVG</b> (Градиентный фон)<br>Клик или Drop</p>
46
+ <input type="file" id="svgInput" accept=".svg" style="display:none">
47
+ </div>
48
+ <input type="text" id="textInput" placeholder="Или текст (A, ★, B и т.д.)" style="padding:10px; background:#444; border:none; border-radius:5px; color:white; font-weight:900; font-size:16px;">
49
+
50
+ <div>
51
+ <label>Масштаб: <span id="scaleVal">100%</span></label>
52
+ <input type="range" id="scale" min="10" max="200" value="100">
53
+ </div>
54
+ <div style="display:flex; gap:10px;">
55
+ <div style="flex:1"><label>X:</label><input type="range" id="offX" min="-256" max="256" value="0"></div>
56
+ <div style="flex:1"><label>Y:</label><input type="range" id="offY" min="-256" max="256" value="0"></div>
57
+ </div>
58
+
59
+ <div class="section-title">Стикеры (PNG/JPG)</div>
60
+ <button class="btn-secondary" id="addSticker">+ Добавить стикер</button>
61
+ <input type="file" id="stickerInput" accept="image/png,image/jpeg" multiple style="display:none">
62
+
63
+ <button class="btn-primary" id="download">СКАЧАТЬ PNG</button>
64
+ </div>
65
+ </div>
66
+
67
+ <script>
68
+ const canvas = document.getElementById('canvas');
69
+ const ctx = canvas.getContext('2d');
70
+ const BG = '#E13839';
71
+
72
+ let base = {type:'none', obj:null, scale:1, x:0, y:0};
73
+ let stickers = [];
74
+ let selected = -1;
75
+ let dragging = false, rotating = false, resizing = false;
76
+ let startX, startY, startRot, startDist, startScale;
77
+
78
+ function draw() {
79
+ ctx.fillStyle = BG;
80
+ ctx.fillRect(0,0,512,512);
81
+
82
+ if (base.type !== 'none') drawBase();
83
+ stickers.forEach((s,i) => {
84
+ ctx.save();
85
+ ctx.translate(s.x, s.y);
86
+ ctx.rotate(s.rot);
87
+ ctx.drawImage(s.img, -s.w/2, -s.h/2, s.w, s.h);
88
+ if (i===selected) drawHandles(s);
89
+ ctx.restore();
90
+ });
91
+ }
92
+
93
+ function drawBase() {
94
+ const tmp = document.createElement('canvas');
95
+ tmp.width = tmp.height = 512;
96
+ const t = tmp.getContext('2d');
97
+ t.translate(256 + base.x, 256 + base.y);
98
+ t.scale(base.scale, base.scale);
99
+
100
+ if (base.type==='image') {
101
+ const ratio = base.obj.height / base.obj.width;
102
+ t.drawImage(base.obj, -150, -150*ratio, 300, 300*ratio);
103
+ } else if (base.type==='text') {
104
+ t.font = "900 300px 'Nunito'";
105
+ t.textAlign = "center";
106
+ t.textBaseline = "middle";
107
+ t.fillStyle = "#000";
108
+ t.fillText(base.obj, 0, 20);
109
+ }
110
+
111
+ t.globalCompositeOperation = 'source-in';
112
+ const grad = t.createLinearGradient(512,0,0,512);
113
+ grad.addColorStop(0,"#FFFFFF");
114
+ grad.addColorStop(1,"#FFACC7");
115
+ t.fillStyle = grad;
116
+ t.setTransform(1,0,0,1,0,0);
117
+ t.fillRect(0,0,512,512);
118
+ ctx.drawImage(tmp,0,0);
119
+ }
120
+
121
+ function drawHandles(s) {
122
+ ctx.strokeStyle = "#FFF";
123
+ ctx.lineWidth = 2;
124
+ ctx.setLineDash([5,5]);
125
+ ctx.strokeRect(-s.w/2, -s.h/2, s.w, s.h);
126
+ ctx.setLineDash([]);
127
+
128
+ ctx.fillStyle = "#E13839";
129
+ ctx.beginPath();
130
+ ctx.arc(0, -s.h/2 - 25, 8, 0, 6.28);
131
+ ctx.fill();
132
+ ctx.stroke();
133
+
134
+ ctx.beginPath();
135
+ ctx.arc(s.w/2, s.h/2, 8, 0, 6.28);
136
+ ctx.fill();
137
+ ctx.stroke();
138
+ }
139
+
140
+ function hitTest(x,y) {
141
+ for (let i=stickers.length-1; i>=0; i--) {
142
+ const s = stickers[i];
143
+ const dx = x - s.x;
144
+ const dy = y - s.y;
145
+ const rx = dx * Math.cos(-s.rot) - dy * Math.sin(-s.rot);
146
+ const ry = dx * Math.sin(-s.rot) + dy * Math.cos(-s.rot);
147
+ if (Math.abs(rx) <= s.w/2 && Math.abs(ry) <= s.h/2) return i;
148
+ }
149
+ return -1;
150
+ }
151
+
152
+ function getHandle(x,y,s) {
153
+ const dx = x - s.x;
154
+ const dy = y - s.y;
155
+ const rx = dx * Math.cos(-s.rot) - dy * Math.sin(-s.rot);
156
+ const ry = dx * Math.sin(-s.rot) + dy * Math.cos(-s.rot);
157
+ if (Math.hypot(rx, ry + s.h/2 + 25) < 20) return 'rot';
158
+ if (Math.hypot(rx - s.w/2, ry - s.h/2) < 20) return 'resize';
159
+ return 'move';
160
+ }
161
+
162
+ // === Обработчики ===
163
+ document.getElementById('dropBase').onclick = () => document.getElementById('svgInput').click();
164
+ document.getElementById('svgInput').onchange = e => {
165
+ const f = e.target.files[0];
166
+ if (!f) return;
167
+ const r = new FileReader();
168
+ r.onload = ev => {
169
+ const img = new Image();
170
+ img.onload = () => {
171
+ base = {type:'image', obj:img, scale:1, x:0, y:0};
172
+ document.getElementById('textInput').value = '';
173
+ draw();
174
+ };
175
+ img.src = ev.target.result;
176
+ };
177
+ r.readAsDataURL(f);
178
+ };
179
+
180
+ document.getElementById('textInput').oninput = e => {
181
+ if (e.target.value.trim()) {
182
+ base = {type:'text', obj:e.target.value.trim(), scale:1, x:0, y:0};
183
+ draw();
184
+ } else if (!document.getElementById('svgInput').files.length) {
185
+ base = {type:'none', obj:null};
186
+ draw();
187
+ }
188
+ };
189
+
190
+ document.getElementById('scale').oninput = e => {
191
+ base.scale = e.target.value / 100;
192
+ document.getElementById('scaleVal').textContent = e.target.value + '%';
193
+ draw();
194
+ };
195
+ document.getElementById('offX').oninput = e => { base.x = +e.target.value; draw(); };
196
+ document.getElementById('offY').oninput = e => { base.y = +e.target.value; draw(); };
197
+
198
+ document.getElementById('addSticker').onclick = () => document.getElementById('stickerInput').click();
199
+ document.getElementById('stickerInput').onchange = e => {
200
+ [...e.target.files].forEach(f => {
201
+ const r = new FileReader();
202
+ r.onload = ev => {
203
+ const img = new Image();
204
+ img.onload = () => {
205
+ let w = img.width, h = img.height;
206
+ if (w > h && w > 200) { h = 200 * h/w; w = 200; }
207
+ if (h > w && h > 200) { w = 200 * w/h; h = 200; }
208
+ stickers.push({img, x:256, y:256, w, h, rot:0});
209
+ selected = stickers.length-1;
210
+ draw();
211
+ };
212
+ img.src = ev.target.result;
213
+ };
214
+ r.readAsDataURL(f);
215
+ });
216
+ };
217
+
218
+ document.getElementById('download').onclick = () => {
219
+ selected = -1; draw();
220
+ canvas.toBlob(blob => {
221
+ const url = URL.createObjectURL(blob);
222
+ const a = document.createElement('a');
223
+ a.href = url; a.download = 'SWAGA_ICON.png';
224
+ a.click();
225
+ });
226
+ };
227
+
228
+ // Mouse / Touch
229
+ let touches = [];
230
+ canvas.onmousedown = canvas.ontouchstart = e => {
231
+ e.preventDefault();
232
+ const pos = e.touches ? e.touches[0] : e;
233
+ const rect = canvas.getBoundingClientRect();
234
+ const x = (pos.clientX - rect.left) * 512 / rect.width;
235
+ const y = (pos.clientY - rect.top) * 512 / rect.height;
236
+
237
+ if (selected !== -1) {
238
+ const handle = getHandle(x,y,stickers[selected]);
239
+ if (handle === 'rot') { rotating = true; startRot = Math.atan2(y - stickers[selected].y, x - stickers[selected].x) - stickers[selected].rot; }
240
+ else if (handle === 'resize') { resizing = true; startDist = Math.hypot(x - stickers[selected].x, y - stickers[selected].y); startScale = stickers[selected].w; }
241
+ else { dragging = true; }
242
+ }
243
+ if (!dragging && !rotating && !resizing) {
244
+ const hit = hitTest(x,y);
245
+ if (hit !== -1) { selected = hit; dragging = true; }
246
+ else selected = -1;
247
+ }
248
+ startX = x; startY = y;
249
+ draw();
250
+ };
251
+
252
+ canvas.onmousemove = canvas.ontouchmove = e => {
253
+ e.preventDefault();
254
+ const pos = e.touches ? e.touches[0] : e;
255
+ const rect = canvas.getBoundingClientRect();
256
+ const x = (pos.clientX - rect.left) * 512 / rect.width;
257
+ const y = (pos.clientY - rect.top) * 512 / rect.height;
258
+
259
+ if (selected === -1) return;
260
+
261
+ const s = stickers[selected];
262
+ if (dragging) {
263
+ s.x += x - startX;
264
+ s.y += y - startY;
265
+ startX = x; startY = y;
266
+ } else if (rotating) {
267
+ s.rot = Math.atan2(y - s.y, x - s.x) - startRot;
268
+ } else if (resizing) {
269
+ const dist = Math.hypot(x - s.x, y - s.y);
270
+ let scale = (dist / startDist);
271
+ let newW = startScale * scale;
272
+ if (newW < 20) newW = 20;
273
+ s.w = newW;
274
+ s.h = newW * (s.img.height / s.img.width);
275
+ }
276
+ draw();
277
+ };
278
+
279
+ canvas.onmouseup = canvas.ontouchend = () => {
280
+ dragging = rotating = resizing = false;
281
+ };
282
+
283
+ window.onkeydown = e => {
284
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selected !== -1) {
285
+ stickers.splice(selected, 1);
286
+ selected = -1;
287
+ draw();
288
+ }
289
+ };
290
+
291
+ draw();
292
+ </script>
293
+ </body>
294
+ </html>
295
+ """
296
+
297
+ def create_app():
298
+ with gr.Blocks() as demo:
299
+ gr.HTML(html_content)
300
+ return demo
301
+
302
+ if __name__ == "__main__":
303
+ create_app().launch()