MetricMogul commited on
Commit
f2a639d
·
verified ·
1 Parent(s): 8dc66af

Update canvas_editor.py

Browse files
Files changed (1) hide show
  1. canvas_editor.py +146 -72
canvas_editor.py CHANGED
@@ -4,6 +4,9 @@ import time
4
  from urllib.parse import quote
5
 
6
 
 
 
 
7
  def file_url(path: str) -> str:
8
  abs_path = os.path.abspath(path)
9
  return f"/gradio_api/file={quote(abs_path, safe='/')}"
@@ -132,7 +135,7 @@ CSS_TEMPLATE = """
132
  }
133
  """
134
 
135
- JS_ON_LOAD = r"""
136
  const scene = element.querySelector(".se-scene");
137
  const btnSmaller = element.querySelector('[data-action="smaller"]');
138
  const btnBigger = element.querySelector('[data-action="bigger"]');
@@ -144,48 +147,114 @@ let lastRenderId = null;
144
  let selected = null;
145
  let spriteCounter = 0;
146
 
147
- function clearSelection() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  if (selected) selected.classList.remove("selected");
149
  selected = null;
150
- }
151
 
152
- function selectSprite(node) {
153
  clearSelection();
154
  selected = node;
155
  selected.classList.add("selected");
156
- }
157
 
158
- function clampWidth(newWidth, sceneWidth) {
159
  const minW = 40;
160
  const maxW = Math.max(minW, sceneWidth * 1.5);
161
  return Math.max(minW, Math.min(maxW, newWidth));
162
- }
163
 
164
- function resizeSelected(multiplier) {
165
  if (!selected) return;
166
 
167
  const sceneRect = scene.getBoundingClientRect();
168
  const currentWidth = selected.getBoundingClientRect().width;
169
  const newWidth = clampWidth(currentWidth * multiplier, sceneRect.width);
170
 
171
- selected.style.width = `${newWidth}px`;
172
- }
 
173
 
174
- function deleteSelected() {
175
  if (!selected) return;
176
  const node = selected;
177
  clearSelection();
178
  node.remove();
179
- }
 
180
 
181
- function makeDraggable(node) {
182
  let startX = 0;
183
  let startY = 0;
184
  let startLeft = 0;
185
  let startTop = 0;
186
  let dragging = false;
187
 
188
- node.addEventListener("pointerdown", (e) => {
189
  e.preventDefault();
190
  e.stopPropagation();
191
 
@@ -200,74 +269,75 @@ function makeDraggable(node) {
200
  startY = e.clientY;
201
  startLeft = parseFloat(node.style.left || "0");
202
  startTop = parseFloat(node.style.top || "0");
203
- });
204
 
205
- node.addEventListener("pointermove", (e) => {
206
  if (!dragging) return;
207
  e.preventDefault();
208
 
209
  const dx = e.clientX - startX;
210
  const dy = e.clientY - startY;
211
 
212
- node.style.left = `${startLeft + dx}px`;
213
- node.style.top = `${startTop + dy}px`;
214
- });
215
 
216
- function endDrag(e) {
217
  if (!dragging) return;
218
  dragging = false;
219
  node.classList.remove("dragging");
220
- try {
221
  node.releasePointerCapture(e.pointerId);
222
- } catch (_) {}
223
- }
 
224
 
225
  node.addEventListener("pointerup", endDrag);
226
  node.addEventListener("pointercancel", endDrag);
227
- }
228
 
229
- function createSprite(item, indexOffset = 0) {
230
  const sceneRect = scene.getBoundingClientRect();
231
  const baseW = Math.max(140, sceneRect.width * 0.28);
232
 
233
  const img = document.createElement("img");
234
  img.className = "se-sprite";
235
- img.alt = item.name || `sprite-${spriteCounter + 1}`;
236
  img.dataset.assetUrl = item.url;
237
  img.dataset.assetName = item.name || "";
238
- img.style.left = `${30 + indexOffset * 24}px`;
239
- img.style.top = `${30 + indexOffset * 24}px`;
240
- img.style.width = `${baseW}px`;
241
  img.style.zIndex = String(++spriteCounter);
242
 
243
- img.addEventListener("click", (e) => {
244
  e.stopPropagation();
245
  selectSprite(img);
246
- });
247
 
248
- img.addEventListener("load", () => {
249
- // ok
250
- });
251
 
252
- img.addEventListener("error", () => {
253
  console.error("Failed to load sprite:", item.url);
254
- });
255
 
256
  img.src = item.url;
257
 
258
  makeDraggable(img);
259
  scene.appendChild(img);
260
- }
261
 
262
- function render() {
263
- const raw = props.value || '{"render_id": null, "mode": "append", "items": []}';
264
 
265
  let payload;
266
- try {
267
  payload = JSON.parse(raw);
268
- } catch (e) {
269
- payload = { render_id: null, mode: "append", items: [] };
270
- }
271
 
272
  const renderId = payload.render_id;
273
  const mode = payload.mode || "append";
@@ -276,60 +346,64 @@ function render() {
276
  if (renderId === lastRenderId) return;
277
  lastRenderId = renderId;
278
 
279
- if (mode === "reset") {
280
  scene.innerHTML = "";
281
  clearSelection();
282
- }
 
283
 
284
  const existingUrls = new Set(
285
  Array.from(scene.querySelectorAll(".se-sprite")).map((node) => node.dataset.assetUrl)
286
  );
287
 
288
- items.forEach((item, i) => {
289
  if (!item.url) return;
290
 
291
- if (mode === "append" && existingUrls.has(item.url)) {
292
  return;
293
- }
294
 
295
  createSprite(item, i + scene.querySelectorAll(".se-sprite").length);
296
- });
297
- }
298
 
299
- scene.addEventListener("click", () => {
 
 
 
300
  clearSelection();
301
- });
302
 
303
- if (btnSmaller) {
304
- btnSmaller.addEventListener("click", () => {
305
  resizeSelected(0.85);
306
- });
307
- }
308
 
309
- if (btnBigger) {
310
- btnBigger.addEventListener("click", () => {
311
  resizeSelected(1.15);
312
- });
313
- }
314
 
315
- if (btnDelete) {
316
- btnDelete.addEventListener("click", () => {
317
  deleteSelected();
318
- });
319
- }
320
 
321
- if (btnClear) {
322
- btnClear.addEventListener("click", () => {
323
  clearSelection();
324
- });
325
- }
326
 
327
- if (btnClearScene) {
328
- btnClearScene.addEventListener("click", () => {
329
  scene.innerHTML = "";
330
  clearSelection();
331
- });
332
- }
 
333
 
334
  render();
335
  setInterval(render, 200);
 
4
  from urllib.parse import quote
5
 
6
 
7
+ EXPORT_TEXTBOX_ID = "scene_png_data"
8
+
9
+
10
  def file_url(path: str) -> str:
11
  abs_path = os.path.abspath(path)
12
  return f"/gradio_api/file={quote(abs_path, safe='/')}"
 
135
  }
136
  """
137
 
138
+ JS_ON_LOAD = rf"""
139
  const scene = element.querySelector(".se-scene");
140
  const btnSmaller = element.querySelector('[data-action="smaller"]');
141
  const btnBigger = element.querySelector('[data-action="bigger"]');
 
147
  let selected = null;
148
  let spriteCounter = 0;
149
 
150
+ function getExportInput() {{
151
+ return document.querySelector("#{EXPORT_TEXTBOX_ID} textarea, #{EXPORT_TEXTBOX_ID} input");
152
+ }}
153
+
154
+ function setExportValue(value) {{
155
+ const target = getExportInput();
156
+ if (!target) return;
157
+
158
+ if (target.value === value) return;
159
+
160
+ target.value = value;
161
+ target.dispatchEvent(new Event("input", {{ bubbles: true }}));
162
+ target.dispatchEvent(new Event("change", {{ bubbles: true }}));
163
+ }}
164
+
165
+ function exportSceneToDataUrl() {{
166
+ const sprites = Array.from(scene.querySelectorAll(".se-sprite"));
167
+ if (!sprites.length) return "";
168
+
169
+ const scale = 2;
170
+ const width = Math.max(1, Math.round(scene.clientWidth));
171
+ const height = Math.max(1, Math.round(scene.clientHeight));
172
+
173
+ const canvas = document.createElement("canvas");
174
+ canvas.width = width * scale;
175
+ canvas.height = height * scale;
176
+
177
+ const ctx = canvas.getContext("2d");
178
+ ctx.scale(scale, scale);
179
+ ctx.clearRect(0, 0, width, height);
180
+
181
+ const orderedSprites = sprites.slice().sort((a, b) => {{
182
+ const za = parseInt(a.style.zIndex || "0", 10);
183
+ const zb = parseInt(b.style.zIndex || "0", 10);
184
+ return za - zb;
185
+ }});
186
+
187
+ orderedSprites.forEach((node) => {{
188
+ const left = parseFloat(node.style.left || "0");
189
+ const top = parseFloat(node.style.top || "0");
190
+ const widthPx = parseFloat(node.style.width || "100");
191
+
192
+ let ratio = 1.0;
193
+ if (node.naturalWidth && node.naturalHeight) {{
194
+ ratio = node.naturalHeight / node.naturalWidth;
195
+ }} else {{
196
+ const rect = node.getBoundingClientRect();
197
+ if (rect.width > 0 && rect.height > 0) {{
198
+ ratio = rect.height / rect.width;
199
+ }}
200
+ }}
201
+
202
+ const heightPx = widthPx * ratio;
203
+ ctx.drawImage(node, left, top, widthPx, heightPx);
204
+ }});
205
+
206
+ return canvas.toDataURL("image/png");
207
+ }}
208
+
209
+ function syncExport() {{
210
+ const dataUrl = exportSceneToDataUrl();
211
+ setExportValue(dataUrl);
212
+ }}
213
+
214
+ function clearSelection() {{
215
  if (selected) selected.classList.remove("selected");
216
  selected = null;
217
+ }}
218
 
219
+ function selectSprite(node) {{
220
  clearSelection();
221
  selected = node;
222
  selected.classList.add("selected");
223
+ }}
224
 
225
+ function clampWidth(newWidth, sceneWidth) {{
226
  const minW = 40;
227
  const maxW = Math.max(minW, sceneWidth * 1.5);
228
  return Math.max(minW, Math.min(maxW, newWidth));
229
+ }}
230
 
231
+ function resizeSelected(multiplier) {{
232
  if (!selected) return;
233
 
234
  const sceneRect = scene.getBoundingClientRect();
235
  const currentWidth = selected.getBoundingClientRect().width;
236
  const newWidth = clampWidth(currentWidth * multiplier, sceneRect.width);
237
 
238
+ selected.style.width = `${{newWidth}}px`;
239
+ syncExport();
240
+ }}
241
 
242
+ function deleteSelected() {{
243
  if (!selected) return;
244
  const node = selected;
245
  clearSelection();
246
  node.remove();
247
+ syncExport();
248
+ }}
249
 
250
+ function makeDraggable(node) {{
251
  let startX = 0;
252
  let startY = 0;
253
  let startLeft = 0;
254
  let startTop = 0;
255
  let dragging = false;
256
 
257
+ node.addEventListener("pointerdown", (e) => {{
258
  e.preventDefault();
259
  e.stopPropagation();
260
 
 
269
  startY = e.clientY;
270
  startLeft = parseFloat(node.style.left || "0");
271
  startTop = parseFloat(node.style.top || "0");
272
+ }});
273
 
274
+ node.addEventListener("pointermove", (e) => {{
275
  if (!dragging) return;
276
  e.preventDefault();
277
 
278
  const dx = e.clientX - startX;
279
  const dy = e.clientY - startY;
280
 
281
+ node.style.left = `${{startLeft + dx}}px`;
282
+ node.style.top = `${{startTop + dy}}px`;
283
+ }});
284
 
285
+ function endDrag(e) {{
286
  if (!dragging) return;
287
  dragging = false;
288
  node.classList.remove("dragging");
289
+ try {{
290
  node.releasePointerCapture(e.pointerId);
291
+ }} catch (_) {{}}
292
+ syncExport();
293
+ }}
294
 
295
  node.addEventListener("pointerup", endDrag);
296
  node.addEventListener("pointercancel", endDrag);
297
+ }}
298
 
299
+ function createSprite(item, indexOffset = 0) {{
300
  const sceneRect = scene.getBoundingClientRect();
301
  const baseW = Math.max(140, sceneRect.width * 0.28);
302
 
303
  const img = document.createElement("img");
304
  img.className = "se-sprite";
305
+ img.alt = item.name || `sprite-${{spriteCounter + 1}}`;
306
  img.dataset.assetUrl = item.url;
307
  img.dataset.assetName = item.name || "";
308
+ img.style.left = `${{30 + indexOffset * 24}}px`;
309
+ img.style.top = `${{30 + indexOffset * 24}}px`;
310
+ img.style.width = `${{baseW}}px`;
311
  img.style.zIndex = String(++spriteCounter);
312
 
313
+ img.addEventListener("click", (e) => {{
314
  e.stopPropagation();
315
  selectSprite(img);
316
+ }});
317
 
318
+ img.addEventListener("load", () => {{
319
+ syncExport();
320
+ }});
321
 
322
+ img.addEventListener("error", () => {{
323
  console.error("Failed to load sprite:", item.url);
324
+ }});
325
 
326
  img.src = item.url;
327
 
328
  makeDraggable(img);
329
  scene.appendChild(img);
330
+ }}
331
 
332
+ function render() {{
333
+ const raw = props.value || '{{"render_id": null, "mode": "append", "items": []}}';
334
 
335
  let payload;
336
+ try {{
337
  payload = JSON.parse(raw);
338
+ }} catch (e) {{
339
+ payload = {{ render_id: null, mode: "append", items: [] }};
340
+ }}
341
 
342
  const renderId = payload.render_id;
343
  const mode = payload.mode || "append";
 
346
  if (renderId === lastRenderId) return;
347
  lastRenderId = renderId;
348
 
349
+ if (mode === "reset") {{
350
  scene.innerHTML = "";
351
  clearSelection();
352
+ setExportValue("");
353
+ }}
354
 
355
  const existingUrls = new Set(
356
  Array.from(scene.querySelectorAll(".se-sprite")).map((node) => node.dataset.assetUrl)
357
  );
358
 
359
+ items.forEach((item, i) => {{
360
  if (!item.url) return;
361
 
362
+ if (mode === "append" && existingUrls.has(item.url)) {{
363
  return;
364
+ }}
365
 
366
  createSprite(item, i + scene.querySelectorAll(".se-sprite").length);
367
+ }});
 
368
 
369
+ syncExport();
370
+ }}
371
+
372
+ scene.addEventListener("click", () => {{
373
  clearSelection();
374
+ }});
375
 
376
+ if (btnSmaller) {{
377
+ btnSmaller.addEventListener("click", () => {{
378
  resizeSelected(0.85);
379
+ }});
380
+ }}
381
 
382
+ if (btnBigger) {{
383
+ btnBigger.addEventListener("click", () => {{
384
  resizeSelected(1.15);
385
+ }});
386
+ }}
387
 
388
+ if (btnDelete) {{
389
+ btnDelete.addEventListener("click", () => {{
390
  deleteSelected();
391
+ }});
392
+ }}
393
 
394
+ if (btnClear) {{
395
+ btnClear.addEventListener("click", () => {{
396
  clearSelection();
397
+ }});
398
+ }}
399
 
400
+ if (btnClearScene) {{
401
+ btnClearScene.addEventListener("click", () => {{
402
  scene.innerHTML = "";
403
  clearSelection();
404
+ setExportValue("");
405
+ }});
406
+ }}
407
 
408
  render();
409
  setInterval(render, 200);