tester343 commited on
Commit
aad9bc7
·
verified ·
1 Parent(s): a8edaa9

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +129 -173
app_enhanced.py CHANGED
@@ -36,36 +36,30 @@ os.makedirs(BASE_USER_DIR, exist_ok=True)
36
  # ======================================================
37
 
38
  def create_placeholder_image(text, filename, output_dir):
39
- """Fallback image generator."""
40
- img = np.zeros((800, 800, 3), dtype=np.uint8)
41
- img[:] = (30, 30, 30) # Dark grey
42
-
43
- # Text
44
  font = cv2.FONT_HERSHEY_SIMPLEX
45
- cv2.putText(img, text, (50, 400), font, 1.5, (200, 200, 200), 3, cv2.LINE_AA)
46
- cv2.rectangle(img, (0,0), (800,800), (100,100,100), 10)
47
-
48
  path = os.path.join(output_dir, filename)
49
  cv2.imwrite(path, img)
50
  return filename
51
 
52
  @spaces.GPU(duration=120)
53
  def generate_comic_gpu(video_path, frames_dir, target_pages):
54
- """Extracts 4 frames per page (2x2 Grid)."""
55
  if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
56
  os.makedirs(frames_dir, exist_ok=True)
57
 
58
  cap = cv2.VideoCapture(video_path)
59
- total_frames = 0
60
- duration = 0
61
-
62
- if cap.isOpened():
63
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
64
- fps = cap.get(cv2.CAP_PROP_FPS) or 25
65
- duration = total_frames / fps
66
- else:
67
  print("❌ Video load failed.")
 
68
 
 
 
 
 
69
  panels_per_page = 4
70
  total_panels_needed = int(target_pages) * panels_per_page
71
  frame_files_ordered = []
@@ -76,14 +70,22 @@ def generate_comic_gpu(video_path, frames_dir, target_pages):
76
  cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
77
  ret, frame = cap.read()
78
  fname = f"frame_{i:04d}.png"
 
79
  if ret and frame is not None:
80
- # Square crop
 
 
 
 
 
 
81
  h, w = frame.shape[:2]
82
- min_dim = min(h, w)
83
- sx = (w - min_dim) // 2
84
- sy = (h - min_dim) // 2
85
- frame = frame[sy:sy+min_dim, sx:sx+min_dim]
86
- frame = cv2.resize(frame, (800, 800))
 
87
  cv2.imwrite(os.path.join(frames_dir, fname), frame)
88
  frame_files_ordered.append(fname)
89
  else:
@@ -110,7 +112,7 @@ def generate_comic_gpu(video_path, frames_dir, target_pages):
110
  pg_panels = [{'image': f} for f in p_frames]
111
  pg_bubbles = []
112
  if i == 0:
113
- pg_bubbles.append({'dialog': "Red Dot = Move Center\nBlue Dot = Tilt Line", 'x': '50%', 'y': '50%'})
114
 
115
  pages_data.append({
116
  'panels': pg_panels,
@@ -132,7 +134,7 @@ class ComicGenHost:
132
 
133
  def run(self, pages):
134
  try:
135
- self.write_status("Processing Video...", 20)
136
  data = generate_comic_gpu(self.video_path, self.frames_dir, pages)
137
  with open(os.path.join(self.output_dir, 'data.json'), 'w') as f:
138
  json.dump(data, f)
@@ -154,22 +156,20 @@ INDEX_HTML = '''
154
  <head>
155
  <meta charset="UTF-8">
156
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
- <title>Advanced Comic Maker</title>
158
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
159
  <link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
160
  <style>
161
- body { background: #121212; color: #eee; font-family: sans-serif; margin: 0; user-select: none; }
162
 
163
- /* LAYOUT */
164
  #upload-view { padding: 50px; text-align: center; }
165
- .box { background: #1e1e1e; display: inline-block; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
166
- button { background: #e74c3c; border: none; padding: 10px 20px; font-weight: bold; cursor: pointer; border-radius: 4px; font-size: 16px; color: white; margin: 5px; }
167
- button:hover { background: #c0392b; }
168
 
169
  #editor-view { display: none; padding: 20px; text-align: center; }
170
- .comic-container { display: flex; flex-wrap: wrap; justify-content: center; gap: 40px; margin-top: 20px; padding-bottom: 100px; }
171
 
172
- /* PAGE */
173
  .comic-page {
174
  width: 600px; height: 800px;
175
  background: white;
@@ -179,104 +179,113 @@ INDEX_HTML = '''
179
  overflow: hidden;
180
  }
181
 
182
- /* GRID LOGIC with Clip Path */
183
  .comic-grid {
184
  width: 100%; height: 100%;
185
  position: relative;
186
  background: #000;
 
187
  --x: 50%; /* Center X */
188
  --y: 50%; /* Center Y */
189
- --xt: 50%; /* Top X (Tilt) */
190
- --xb: 50%; /* Bottom X (Tilt) */
191
- --gap: 3px;
 
192
  }
193
 
194
  .panel {
195
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
196
- overflow: hidden;
197
- background: #222;
198
  }
199
 
200
- /* IMAGE ZOOM/PAN */
201
  .panel img {
202
  width: 100%; height: 100%;
203
- object-fit: cover;
204
  transform-origin: center;
205
  cursor: grab;
206
  }
207
  .panel img:active { cursor: grabbing; }
208
 
209
- /* CLIP PATHS FOR TILTED PANELS */
210
  /* Top Left */
211
  .panel:nth-child(1) {
212
- clip-path: polygon(0 0, calc(var(--xt) - var(--gap)), calc(var(--x) - var(--gap)) calc(var(--y) - var(--gap)), 0 calc(var(--y) - var(--gap)));
 
 
 
 
 
213
  z-index: 1;
214
  }
215
  /* Top Right */
216
  .panel:nth-child(2) {
217
- clip-path: polygon(calc(var(--xt) + var(--gap)) 0, 100% 0, 100% calc(var(--y) - var(--gap)), calc(var(--x) + var(--gap)) calc(var(--y) - var(--gap)));
 
 
 
 
 
218
  z-index: 1;
219
  }
220
  /* Bottom Left */
221
  .panel:nth-child(3) {
222
- clip-path: polygon(0 calc(var(--y) + var(--gap)), calc(var(--x) - var(--gap)) calc(var(--y) + var(--gap)), calc(var(--xb) - var(--gap)) 100%, 0 100%);
 
 
 
 
 
223
  z-index: 1;
224
  }
225
  /* Bottom Right */
226
  .panel:nth-child(4) {
227
- clip-path: polygon(calc(var(--x) + var(--gap)) calc(var(--y) + var(--gap)), 100% calc(var(--y) + var(--gap)), 100% 100%, calc(var(--xb) + var(--gap)) 100%);
 
 
 
 
 
228
  z-index: 1;
229
  }
230
 
231
- /* HANDLES */
232
- .handle-center {
233
- position: absolute;
234
- width: 20px; height: 20px;
235
- background: #e74c3c; /* RED */
236
  border: 2px solid white; border-radius: 50%;
237
- left: var(--x); top: var(--y);
238
  transform: translate(-50%, -50%);
239
- cursor: move; z-index: 999;
240
- box-shadow: 0 0 5px black;
241
- }
242
-
243
- .handle-tilt {
244
- position: absolute;
245
- width: 16px; height: 16px;
246
- background: #3498db; /* BLUE */
247
- border: 2px solid white; border-radius: 50%;
248
- left: var(--xt); top: 0%;
249
- transform: translate(-50%, 50%); /* Just below top edge */
250
- cursor: ew-resize; z-index: 999;
251
- box-shadow: 0 0 5px black;
252
  }
 
 
 
 
 
253
 
254
  /* BUBBLES */
255
  .bubble {
256
- position: absolute;
257
- background: white; color: black;
258
- padding: 8px 12px; border-radius: 15px;
259
  font-family: 'Bangers', cursive; letter-spacing: 1px;
260
  border: 2px solid black; z-index: 100; cursor: move;
261
- transform: translate(-50%, -50%);
262
- text-align: center; min-width: 60px;
263
- box-shadow: 4px 4px 0 rgba(0,0,0,0.2);
264
  }
265
 
266
  .toolbar {
267
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
268
- background: #333; padding: 10px 20px; border-radius: 50px;
269
- display: flex; gap: 10px; z-index: 1000; box-shadow: 0 5px 20px rgba(0,0,0,0.6);
 
270
  }
271
- .info-tip { color: #888; margin-top: 10px; font-size: 0.9em; }
272
  </style>
273
  </head>
274
  <body>
275
 
276
  <div id="upload-view">
277
  <div class="box">
278
- <h1>🎞️ Advanced Comic Maker</h1>
279
- <p>Zoom, Pan, Tilt & Resize!</p>
280
  <input type="file" id="fileIn" accept="video/*"><br><br>
281
  <label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
282
  <br><br>
@@ -288,22 +297,17 @@ INDEX_HTML = '''
288
  <div id="editor-view">
289
  <div class="comic-container" id="container"></div>
290
  <div class="toolbar">
291
- <button onclick="addBubble()">💬 Text</button>
292
- <button onclick="downloadAll()">💾 Save</button>
293
  <button style="background:#555" onclick="location.reload()">↺ Reset</button>
294
  </div>
295
- <div class="info-tip">
296
- <b>Controls:</b> Red Dot = Resize Center | Blue Dot = Tilt | Scroll Image = Zoom | Drag Image = Pan
297
- </div>
298
  </div>
299
 
300
  <script>
301
  let sid = 'S' + Date.now();
302
- let dragType = null; // 'center', 'tilt', 'bubble', 'pan'
303
  let activeObj = null;
304
  let dragStart = {x:0, y:0};
305
-
306
- // IMAGE STATE STORAGE: { imgId: {scale:1, tx:0, ty:0} }
307
  let imgStates = new Map();
308
 
309
  async function startUpload() {
@@ -337,61 +341,54 @@ INDEX_HTML = '''
337
  data.forEach(pg => {
338
  let pDiv = document.createElement('div');
339
  pDiv.className = 'comic-page';
340
-
341
  let grid = document.createElement('div');
342
  grid.className = 'comic-grid';
343
 
344
  // Panels
345
- pg.panels.forEach((pan, idx) => {
346
  let div = document.createElement('div');
347
  div.className = 'panel';
348
  let img = document.createElement('img');
349
  img.src = `/frames/${pan.image}?sid=${sid}`;
350
  img.id = `img-${Math.random().toString(36).substr(2,9)}`;
351
 
352
- // Initialize State
353
  imgStates.set(img.id, {s: 1, tx: 0, ty: 0});
354
-
355
- // Zoom Listener
356
  img.onwheel = (e) => handleZoom(e, img);
357
- // Pan Listener (Start)
358
  img.onmousedown = (e) => {
359
  e.preventDefault(); e.stopPropagation();
360
- dragType = 'pan';
361
- activeObj = img;
362
  dragStart = {x: e.clientX, y: e.clientY};
363
  };
364
-
365
  div.appendChild(img);
366
  grid.appendChild(div);
367
  });
368
 
369
- // Center Handle (Red)
370
- let hc = document.createElement('div');
371
- hc.className = 'handle-center';
372
- hc.onmousedown = (e) => {
373
- e.stopPropagation();
374
- dragType = 'center'; activeObj = grid;
375
- };
376
- grid.appendChild(hc);
377
-
378
- // Tilt Handle (Blue)
379
- let ht = document.createElement('div');
380
- ht.className = 'handle-tilt';
381
- ht.onmousedown = (e) => {
382
- e.stopPropagation();
383
- dragType = 'tilt'; activeObj = grid;
384
- };
385
- grid.appendChild(ht);
386
-
387
- // Bubbles
388
- if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
389
 
 
 
 
390
  pDiv.appendChild(grid);
391
  con.appendChild(pDiv);
392
  });
393
  }
394
 
 
 
 
 
 
 
 
 
 
 
395
  function createBubble(txt, parent) {
396
  let b = document.createElement('div');
397
  b.className = 'bubble';
@@ -408,13 +405,11 @@ INDEX_HTML = '''
408
  window.addBubble = () => createBubble("New Text");
409
 
410
  // === INTERACTION LOGIC ===
411
-
412
- // Zoom
413
  function handleZoom(e, img) {
414
  e.preventDefault();
415
  let st = imgStates.get(img.id);
416
  let delta = e.deltaY * -0.001;
417
- st.s = Math.min(Math.max(0.5, st.s + delta), 5); // Limit Zoom 0.5x to 5x
418
  updateImgTransform(img);
419
  }
420
 
@@ -423,49 +418,39 @@ INDEX_HTML = '''
423
  img.style.transform = `translate(${st.tx}px, ${st.ty}px) scale(${st.s})`;
424
  }
425
 
426
- // Global Move
427
  document.addEventListener('mousemove', (e) => {
428
  if(!dragType) return;
429
 
430
- // 1. Resize Center (Red Dot)
431
  if(dragType === 'center') {
432
  let rect = activeObj.getBoundingClientRect();
433
  let x = (e.clientX - rect.left) / rect.width * 100;
434
  let y = (e.clientY - rect.top) / rect.height * 100;
435
-
436
- // Allow 0 to 100%
437
- x = Math.max(0, Math.min(100, x));
438
- y = Math.max(0, Math.min(100, y));
439
-
440
- activeObj.style.setProperty('--x', x + '%');
441
- activeObj.style.setProperty('--y', y + '%');
442
-
443
- // Recalculate Tilt Bottom (Geometry)
444
- updateTiltGeometry(activeObj);
445
  }
446
-
447
- // 2. Tilt (Blue Dot)
448
- else if(dragType === 'tilt') {
449
  let rect = activeObj.getBoundingClientRect();
450
  let x = (e.clientX - rect.left) / rect.width * 100;
451
- x = Math.max(0, Math.min(100, x)); // 0-100%
452
-
453
- activeObj.style.setProperty('--xt', x + '%');
454
- updateTiltGeometry(activeObj);
455
  }
456
-
457
- // 3. Pan Image
 
 
 
 
 
458
  else if(dragType === 'pan') {
459
  let dx = e.clientX - dragStart.x;
460
  let dy = e.clientY - dragStart.y;
461
  let st = imgStates.get(activeObj.id);
462
- st.tx += dx;
463
- st.ty += dy;
464
  dragStart = {x: e.clientX, y: e.clientY};
465
  updateImgTransform(activeObj);
466
  }
467
-
468
- // 4. Move Bubble
469
  else if(dragType === 'bubble') {
470
  let rect = activeObj.parentElement.getBoundingClientRect();
471
  activeObj.style.left = (e.clientX - rect.left) + 'px';
@@ -474,47 +459,18 @@ INDEX_HTML = '''
474
  });
475
 
476
  document.addEventListener('mouseup', () => { dragType = null; activeObj = null; });
477
-
478
- // Calculates the Bottom X based on Top X and Center X to form a straight line
479
- function updateTiltGeometry(grid) {
480
- // Need to read computed styles roughly or rely on inline styles
481
- // Since we set properties on the style attribute, we can parse them
482
- let cx = parseFloat(grid.style.getPropertyValue('--x')) || 50;
483
- let cy = parseFloat(grid.style.getPropertyValue('--y')) || 50;
484
- let xt = parseFloat(grid.style.getPropertyValue('--xt')) || 50;
485
-
486
- // Math: We have Point Top (xt, 0) and Point Center (cx, cy).
487
- // We want Point Bottom (xb, 100) to be on the same line.
488
- // Slope m = (cy - 0) / (cx - xt) = cy / (cx - xt)
489
- // Line eq: y - 0 = m * (x - xt) => y = m(x - xt)
490
- // At bottom, y = 100.
491
- // 100 = (cy / (cx - xt)) * (xb - xt)
492
- // 100 * (cx - xt) / cy = xb - xt
493
- // xb = xt + (100/cy) * (cx - xt)
494
-
495
- if(cy === 0) cy = 0.1; // Avoid divide by zero
496
-
497
- let xb = xt + (100 / cy) * (cx - xt);
498
-
499
- // Clamp visually so it doesn't fly off screen too wildly
500
- // xb = Math.max(-50, Math.min(150, xb));
501
-
502
- grid.style.setProperty('--xb', xb + '%');
503
- }
504
 
505
  window.downloadAll = async () => {
506
  let pgs = document.querySelectorAll('.comic-page');
507
  for(let i=0; i<pgs.length; i++) {
508
- // Hide handles
509
- let handles = pgs[i].querySelectorAll('.handle-center, .handle-tilt');
510
  handles.forEach(h => h.style.display='none');
511
-
512
  let url = await htmlToImage.toPng(pgs[i]);
513
  let a = document.createElement('a');
514
  a.download = `comic_page_${i+1}.png`;
515
  a.href = url;
516
  a.click();
517
-
518
  handles.forEach(h => h.style.display='block');
519
  }
520
  };
 
36
  # ======================================================
37
 
38
  def create_placeholder_image(text, filename, output_dir):
39
+ img = np.zeros((1200, 800, 3), dtype=np.uint8)
40
+ img[:] = (30, 30, 30)
 
 
 
41
  font = cv2.FONT_HERSHEY_SIMPLEX
42
+ cv2.putText(img, text, (50, 600), font, 2, (200, 200, 200), 4, cv2.LINE_AA)
43
+ cv2.rectangle(img, (0,0), (800,1200), (100,100,100), 10)
 
44
  path = os.path.join(output_dir, filename)
45
  cv2.imwrite(path, img)
46
  return filename
47
 
48
  @spaces.GPU(duration=120)
49
  def generate_comic_gpu(video_path, frames_dir, target_pages):
50
+ """Extracts frames WITHOUT cropping them (Preserves content)."""
51
  if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
52
  os.makedirs(frames_dir, exist_ok=True)
53
 
54
  cap = cv2.VideoCapture(video_path)
55
+ if not cap.isOpened():
 
 
 
 
 
 
 
56
  print("❌ Video load failed.")
57
+ return []
58
 
59
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
60
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
61
+ duration = total_frames / fps
62
+
63
  panels_per_page = 4
64
  total_panels_needed = int(target_pages) * panels_per_page
65
  frame_files_ordered = []
 
70
  cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
71
  ret, frame = cap.read()
72
  fname = f"frame_{i:04d}.png"
73
+
74
  if ret and frame is not None:
75
+ # FIX: Do NOT crop to square. Just resize to reasonable resolution.
76
+ # Maintains visual data.
77
+ # We resize to a standard width (e.g. 800) but keep aspect ratio relative
78
+ # or just force a high res fit.
79
+
80
+ # Let's resize to 800xWidth to ensure it fits nicely in panels
81
+ # but we won't cut pixels off.
82
  h, w = frame.shape[:2]
83
+
84
+ # Simple Resize (Distortion is usually okay for comics,
85
+ # but keeping aspect ratio is better. Let's just standard resize
86
+ # so the frontend Object-Fit handles the rest).
87
+ frame = cv2.resize(frame, (800, 1000))
88
+
89
  cv2.imwrite(os.path.join(frames_dir, fname), frame)
90
  frame_files_ordered.append(fname)
91
  else:
 
112
  pg_panels = [{'image': f} for f in p_frames]
113
  pg_bubbles = []
114
  if i == 0:
115
+ pg_bubbles.append({'dialog': "Red: Center\nBlue: Top Tilt\nGreen: Bottom Tilt", 'x': '50%', 'y': '50%'})
116
 
117
  pages_data.append({
118
  'panels': pg_panels,
 
134
 
135
  def run(self, pages):
136
  try:
137
+ self.write_status("Processing...", 20)
138
  data = generate_comic_gpu(self.video_path, self.frames_dir, pages)
139
  with open(os.path.join(self.output_dir, 'data.json'), 'w') as f:
140
  json.dump(data, f)
 
156
  <head>
157
  <meta charset="UTF-8">
158
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
159
+ <title>Ultimate Comic Editor</title>
160
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
161
  <link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
162
  <style>
163
+ body { background: #1a1a1a; color: #eee; font-family: sans-serif; margin: 0; user-select: none; }
164
 
 
165
  #upload-view { padding: 50px; text-align: center; }
166
+ .box { background: #2d2d2d; display: inline-block; padding: 40px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
167
+ button { background: #e74c3c; border: none; padding: 12px 24px; font-weight: bold; cursor: pointer; border-radius: 6px; font-size: 16px; color: white; margin: 5px; transition: 0.2s; }
168
+ button:hover { transform: scale(1.05); }
169
 
170
  #editor-view { display: none; padding: 20px; text-align: center; }
171
+ .comic-container { display: flex; flex-wrap: wrap; justify-content: center; gap: 40px; margin-top: 20px; padding-bottom: 120px; }
172
 
 
173
  .comic-page {
174
  width: 600px; height: 800px;
175
  background: white;
 
179
  overflow: hidden;
180
  }
181
 
182
+ /* === GRID CSS === */
183
  .comic-grid {
184
  width: 100%; height: 100%;
185
  position: relative;
186
  background: #000;
187
+ /* Coordinate System */
188
  --x: 50%; /* Center X */
189
  --y: 50%; /* Center Y */
190
+ --xt: 50%; /* Top Edge X */
191
+ --xb: 50%; /* Bottom Edge X */
192
+
193
+ --gap: 3px; /* Gap thickness */
194
  }
195
 
196
  .panel {
197
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
198
+ overflow: hidden; background: #222;
 
199
  }
200
 
 
201
  .panel img {
202
  width: 100%; height: 100%;
203
+ object-fit: cover; /* Ensures image fills panel. User zooms/pans to adjust. */
204
  transform-origin: center;
205
  cursor: grab;
206
  }
207
  .panel img:active { cursor: grabbing; }
208
 
209
+ /* === DYNAMIC CLIP PATHS === */
210
  /* Top Left */
211
  .panel:nth-child(1) {
212
+ clip-path: polygon(
213
+ 0 0,
214
+ calc(var(--xt) - var(--gap)) 0,
215
+ calc(var(--x) - var(--gap)) calc(var(--y) - var(--gap)),
216
+ 0 calc(var(--y) - var(--gap))
217
+ );
218
  z-index: 1;
219
  }
220
  /* Top Right */
221
  .panel:nth-child(2) {
222
+ clip-path: polygon(
223
+ calc(var(--xt) + var(--gap)) 0,
224
+ 100% 0,
225
+ 100% calc(var(--y) - var(--gap)),
226
+ calc(var(--x) + var(--gap)) calc(var(--y) - var(--gap))
227
+ );
228
  z-index: 1;
229
  }
230
  /* Bottom Left */
231
  .panel:nth-child(3) {
232
+ clip-path: polygon(
233
+ 0 calc(var(--y) + var(--gap)),
234
+ calc(var(--x) - var(--gap)) calc(var(--y) + var(--gap)),
235
+ calc(var(--xb) - var(--gap)) 100%,
236
+ 0 100%
237
+ );
238
  z-index: 1;
239
  }
240
  /* Bottom Right */
241
  .panel:nth-child(4) {
242
+ clip-path: polygon(
243
+ calc(var(--x) + var(--gap)) calc(var(--y) + var(--gap)),
244
+ 100% calc(var(--y) + var(--gap)),
245
+ 100% 100%,
246
+ calc(var(--xb) + var(--gap)) 100%
247
+ );
248
  z-index: 1;
249
  }
250
 
251
+ /* === HANDLES === */
252
+ .handle {
253
+ position: absolute; width: 22px; height: 22px;
 
 
254
  border: 2px solid white; border-radius: 50%;
 
255
  transform: translate(-50%, -50%);
256
+ z-index: 999; cursor: pointer;
257
+ box-shadow: 0 2px 5px rgba(0,0,0,0.8);
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
+ .handle:hover { transform: translate(-50%, -50%) scale(1.2); }
260
+
261
+ .h-center { background: #e74c3c; left: var(--x); top: var(--y); cursor: move; }
262
+ .h-top { background: #3498db; left: var(--xt); top: 1%; cursor: ew-resize; }
263
+ .h-bottom { background: #2ecc71; left: var(--xb); top: 99%; cursor: ew-resize; }
264
 
265
  /* BUBBLES */
266
  .bubble {
267
+ position: absolute; background: white; color: black;
268
+ padding: 8px 12px; border-radius: 12px;
 
269
  font-family: 'Bangers', cursive; letter-spacing: 1px;
270
  border: 2px solid black; z-index: 100; cursor: move;
271
+ transform: translate(-50%, -50%); text-align: center;
272
+ box-shadow: 3px 3px 0 rgba(0,0,0,0.2);
 
273
  }
274
 
275
  .toolbar {
276
+ position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
277
+ background: #222; padding: 12px 25px; border-radius: 50px;
278
+ display: flex; gap: 15px; z-index: 1000; box-shadow: 0 5px 20px rgba(0,0,0,0.6);
279
+ border: 1px solid #444;
280
  }
 
281
  </style>
282
  </head>
283
  <body>
284
 
285
  <div id="upload-view">
286
  <div class="box">
287
+ <h1>🎞️ Full Control Comic Maker</h1>
288
+ <p>Independent Upper/Lower Tilt + Full Image View</p>
289
  <input type="file" id="fileIn" accept="video/*"><br><br>
290
  <label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
291
  <br><br>
 
297
  <div id="editor-view">
298
  <div class="comic-container" id="container"></div>
299
  <div class="toolbar">
300
+ <button onclick="addBubble()">💬 Add Text</button>
301
+ <button onclick="downloadAll()">💾 Download</button>
302
  <button style="background:#555" onclick="location.reload()">↺ Reset</button>
303
  </div>
 
 
 
304
  </div>
305
 
306
  <script>
307
  let sid = 'S' + Date.now();
308
+ let dragType = null;
309
  let activeObj = null;
310
  let dragStart = {x:0, y:0};
 
 
311
  let imgStates = new Map();
312
 
313
  async function startUpload() {
 
341
  data.forEach(pg => {
342
  let pDiv = document.createElement('div');
343
  pDiv.className = 'comic-page';
 
344
  let grid = document.createElement('div');
345
  grid.className = 'comic-grid';
346
 
347
  // Panels
348
+ pg.panels.forEach((pan) => {
349
  let div = document.createElement('div');
350
  div.className = 'panel';
351
  let img = document.createElement('img');
352
  img.src = `/frames/${pan.image}?sid=${sid}`;
353
  img.id = `img-${Math.random().toString(36).substr(2,9)}`;
354
 
 
355
  imgStates.set(img.id, {s: 1, tx: 0, ty: 0});
 
 
356
  img.onwheel = (e) => handleZoom(e, img);
 
357
  img.onmousedown = (e) => {
358
  e.preventDefault(); e.stopPropagation();
359
+ dragType = 'pan'; activeObj = img;
 
360
  dragStart = {x: e.clientX, y: e.clientY};
361
  };
 
362
  div.appendChild(img);
363
  grid.appendChild(div);
364
  });
365
 
366
+ // Handles
367
+ // 1. Center (Red)
368
+ let hc = createHandle('h-center', grid, 'center');
369
+ // 2. Top (Blue)
370
+ let ht = createHandle('h-top', grid, 'top');
371
+ // 3. Bottom (Green)
372
+ let hb = createHandle('h-bottom', grid, 'bottom');
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
+ grid.append(hc, ht, hb);
375
+
376
+ if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
377
  pDiv.appendChild(grid);
378
  con.appendChild(pDiv);
379
  });
380
  }
381
 
382
+ function createHandle(cls, grid, type) {
383
+ let h = document.createElement('div');
384
+ h.className = `handle ${cls}`;
385
+ h.onmousedown = (e) => {
386
+ e.stopPropagation();
387
+ dragType = type; activeObj = grid;
388
+ };
389
+ return h;
390
+ }
391
+
392
  function createBubble(txt, parent) {
393
  let b = document.createElement('div');
394
  b.className = 'bubble';
 
405
  window.addBubble = () => createBubble("New Text");
406
 
407
  // === INTERACTION LOGIC ===
 
 
408
  function handleZoom(e, img) {
409
  e.preventDefault();
410
  let st = imgStates.get(img.id);
411
  let delta = e.deltaY * -0.001;
412
+ st.s = Math.min(Math.max(0.5, st.s + delta), 5);
413
  updateImgTransform(img);
414
  }
415
 
 
418
  img.style.transform = `translate(${st.tx}px, ${st.ty}px) scale(${st.s})`;
419
  }
420
 
 
421
  document.addEventListener('mousemove', (e) => {
422
  if(!dragType) return;
423
 
424
+ // 1. Center (Moves Intersection)
425
  if(dragType === 'center') {
426
  let rect = activeObj.getBoundingClientRect();
427
  let x = (e.clientX - rect.left) / rect.width * 100;
428
  let y = (e.clientY - rect.top) / rect.height * 100;
429
+ activeObj.style.setProperty('--x', clamp(x)+'%');
430
+ activeObj.style.setProperty('--y', clamp(y)+'%');
 
 
 
 
 
 
 
 
431
  }
432
+ // 2. Top Tilt (Moves Top Edge X)
433
+ else if(dragType === 'top') {
 
434
  let rect = activeObj.getBoundingClientRect();
435
  let x = (e.clientX - rect.left) / rect.width * 100;
436
+ activeObj.style.setProperty('--xt', clamp(x)+'%');
 
 
 
437
  }
438
+ // 3. Bottom Tilt (Moves Bottom Edge X)
439
+ else if(dragType === 'bottom') {
440
+ let rect = activeObj.getBoundingClientRect();
441
+ let x = (e.clientX - rect.left) / rect.width * 100;
442
+ activeObj.style.setProperty('--xb', clamp(x)+'%');
443
+ }
444
+ // 4. Pan Image
445
  else if(dragType === 'pan') {
446
  let dx = e.clientX - dragStart.x;
447
  let dy = e.clientY - dragStart.y;
448
  let st = imgStates.get(activeObj.id);
449
+ st.tx += dx; st.ty += dy;
 
450
  dragStart = {x: e.clientX, y: e.clientY};
451
  updateImgTransform(activeObj);
452
  }
453
+ // 5. Bubble
 
454
  else if(dragType === 'bubble') {
455
  let rect = activeObj.parentElement.getBoundingClientRect();
456
  activeObj.style.left = (e.clientX - rect.left) + 'px';
 
459
  });
460
 
461
  document.addEventListener('mouseup', () => { dragType = null; activeObj = null; });
462
+ function clamp(val) { return Math.max(0, Math.min(100, val)); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  window.downloadAll = async () => {
465
  let pgs = document.querySelectorAll('.comic-page');
466
  for(let i=0; i<pgs.length; i++) {
467
+ let handles = pgs[i].querySelectorAll('.handle');
 
468
  handles.forEach(h => h.style.display='none');
 
469
  let url = await htmlToImage.toPng(pgs[i]);
470
  let a = document.createElement('a');
471
  a.download = `comic_page_${i+1}.png`;
472
  a.href = url;
473
  a.click();
 
474
  handles.forEach(h => h.style.display='block');
475
  }
476
  };