tester343 commited on
Commit
7ca93e7
·
verified ·
1 Parent(s): 1481afd

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +365 -774
app_enhanced.py CHANGED
@@ -328,806 +328,397 @@ class EnhancedComicGenerator:
328
  # 🌐 ROUTES & FULL UI
329
  # ======================================================
330
  INDEX_HTML = '''
331
- <!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🎬 Enhanced Comic Generator</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script> <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; } body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
332
-
333
- #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
334
- .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
335
-
336
- #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
337
-
338
- h1 { color: #2c3e50; margin-bottom: 20px; font-weight: 600; }
339
- .file-input { display: none; }
340
- .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
341
- .file-label:hover { background: #34495e; }
342
-
343
- .page-input-group { margin: 20px 0; text-align: left; }
344
- .page-input-group label { font-weight: bold; font-size: 14px; display: block; margin-bottom: 5px; color: #333; }
345
- .page-input-group input { width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box; }
346
-
347
- .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
348
- .submit-btn:hover { background: #d35400; }
349
- .restore-btn { margin-top: 10px; background: #27ae60; color: white; padding: 12px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
350
-
351
- .load-section { margin-top: 30px; padding-top: 20px; border-top: 2px solid #eee; }
352
- .load-input-group { display: flex; gap: 10px; margin-top: 10px; }
353
- .load-input-group input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; text-transform: uppercase; letter-spacing: 2px; text-align: center; }
354
- .load-input-group button { padding: 12px 20px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
355
-
356
- .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
357
- @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
358
-
359
- /* COMIC LAYOUT */
360
- .comic-wrapper { max-width: 1200px; margin: 0 auto; display:flex; flex-direction:column; align-items:center; }
361
- .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; width: 100%; }
362
- .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
363
-
364
- /*
365
- SIZE UPDATE: 1000px width x 712px height
366
- (Matches 350px x 2 tiers + 12px gutter)
367
- Scaled down using transform for viewing
368
- */
369
- .comic-page {
370
- background: white;
371
- width: 1000px;
372
- height: 712px;
373
- box-shadow: 0 4px 10px rgba(0,0,0,0.1);
374
- position: relative;
375
- overflow: hidden;
376
- border: 2px solid #000;
377
- padding: 0;
378
- /* SCALING FOR EDITOR VIEW */
379
- transform-origin: top center;
380
- transform: scale(0.6);
381
- margin-bottom: -280px; /* Compensate for scale empty space */
382
- }
383
-
384
- /* === 5-PANEL TEMPLATE (2 TOP, 3 BOTTOM) === */
385
- /* Background White for 12px Gutter (1.2% width, 1.7% height of 712px) */
386
- .comic-grid { width: 100%; height: 100%; position: relative; background: #ffffff; }
387
-
388
- /* Panel Background White */
389
- .panel {
390
- position: absolute;
391
- overflow: hidden;
392
- background: #ffffff;
393
- cursor: pointer;
394
- border: 0;
395
- display:flex;
396
- justify-content:center;
397
- align-items:center; /* Center image within the panel if object-fit: contain */
398
- }
399
- .panel.selected { z-index: 20; }
400
- .panel.selected img { outline: 3px solid #2196F3; outline-offset: -3px; }
401
-
402
- /*
403
- Reference: 1000px width x 712px height.
404
- Gutter: 12px horizontal (1.2%), 12px vertical (1.7%).
405
- Half Gutter: 0.6% Horz, 0.85% Vert.
406
-
407
- COORDINATES from Green Text:
408
- Row 1 Split: Top=63.2%, Bottom=59.5%
409
- Row 2 Left Split: Top=33.0%, Bottom=35.7%
410
- Row 2 Right Split: Top=64.8%, Bottom=68.2%
411
- Tier Height: 350px (approx 49.15% of 712px)
412
- */
413
-
414
- /* --- ROW 1 (TOP) --- Height 49.15% */
415
-
416
- /* Panel 1: Right ends at (63.2 - 0.6)% / (59.5 - 0.6)% */
417
- .panel:nth-child(1) {
418
- top: 0; left: 0; height: 49.15%; width: 100%;
419
- clip-path: polygon(0% 0%, 62.6% 0%, 58.9% 100%, 0% 100%);
420
- }
421
- /* Panel 2: Left starts at (63.2 + 0.6)% / (59.5 + 0.6)% */
422
- .panel:nth-child(2) {
423
- top: 0; left: 0; height: 49.15%; width: 100%;
424
- clip-path: polygon(63.8% 0%, 100% 0%, 100% 100%, 60.1% 100%);
425
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
- /* --- ROW 2 (BOTTOM) --- Top: 50.85% (Gap ~1.7%), Height: 49.15% */
428
-
429
- /* Panel 3: Right ends at (33.0 - 0.6)% / (35.7 - 0.6)% */
430
- .panel:nth-child(3) {
431
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
432
- clip-path: polygon(0% 0%, 32.4% 0%, 35.1% 100%, 0% 100%);
433
- }
434
- /* Panel 4: Left starts (33.0 + 0.6)% / (35.7 + 0.6)%. Right ends (64.8 - 0.6)% / (68.2 - 0.6)% */
435
- .panel:nth-child(4) {
436
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
437
- clip-path: polygon(33.6% 0%, 64.2% 0%, 67.6% 100%, 36.3% 100%);
438
- }
439
- /* Panel 5: Left starts (64.8 + 0.6)% / (68.2 + 0.6)% */
440
- .panel:nth-child(5) {
441
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
442
- clip-path: polygon(65.4% 0%, 100% 0%, 100% 100%, 68.8% 100%);
443
- }
444
-
445
- /* ====================== */
446
-
447
- /*
448
- IMAGE FIT: 'contain' ensures full image is visible (100% fit) without crop.
449
- Gaps are handled by the white background of the panel/grid.
450
- Removed all transform:scale from here.
451
- */
452
- .panel img {
453
- width: 100%;
454
- height: 100%;
455
- object-fit: contain; /* DEFAULT: Show full image, padded with white */
456
- image-rendering: auto; /* Ensures no blur from scaling */
457
- transform: none !important; /* Force no JS transforms on image itself */
458
- max-width: 100%; /* Force original size */
459
- max-height: 100%; /* Force original size */
460
- display: block; /* Important for proper centering and sizing */
461
- }
462
- /* Toggle classes */
463
- .panel img.fit-cover { object-fit: cover !important; }
464
- .panel img.fit-fill { object-fit: fill !important; }
465
-
466
- .panel img.pannable { cursor: grab; }
467
- .panel img.panning { cursor: grabbing; }
468
-
469
- /* SPEECH BUBBLES */
470
- .speech-bubble {
471
- position: absolute; display: flex; justify-content: center; align-items: center;
472
- width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
473
- z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
474
- font-size: 13px; text-align: center;
475
- overflow: visible;
476
- line-height: 1.2;
477
- --tail-pos: 50%;
478
- }
479
-
480
- .bubble-text {
481
- padding: 0.5em; word-wrap: break-word; white-space: pre-wrap; position: relative;
482
- z-index: 5; pointer-events: none; user-select: none; width: 100%; height: 100%;
483
- overflow: hidden; display: flex; align-items: center; justify-content: center; border-radius: inherit;
484
- }
485
-
486
- .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
487
- .speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; white-space: pre-wrap; }
488
-
489
- /* SPEECH BUBBLE CSS (Tails) */
490
- .speech-bubble.speech {
491
- --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
492
- background: var(--bubble-fill-color, #4ECDC4);
493
- color: var(--bubble-text-color, #fff);
494
- padding: 0;
495
- border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r);
496
- }
497
- .speech-bubble.speech:before {
498
- content: ""; position: absolute; width: var(--b); height: var(--h);
499
- background: inherit; border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
500
- -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
501
- mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
502
- }
503
-
504
- .speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
505
- .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
506
- .speech-bubble.speech.tail-left { border-radius: var(--r); }
507
- .speech-bubble.speech.tail-left:before { right: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(90deg); transform-origin: top right; }
508
- .speech-bubble.speech.tail-right { border-radius: var(--r); }
509
- .speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
510
-
511
- /* Thought/Reaction Styles */
512
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
513
- .speech-bubble.thought::before { display:none; }
514
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
515
- .thought-dot-1 { width: 20px; height: 20px; }
516
- .thought-dot-2 { width: 12px; height: 12px; }
517
- .speech-bubble.thought.pos-bl .thought-dot-1 { left: 20px; bottom: -20px; }
518
- .speech-bubble.thought.pos-bl .thought-dot-2 { left: 10px; bottom: -32px; }
519
- .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
520
- .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
521
-
522
- .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
523
- .speech-bubble.selected .resize-handle { display: block; }
524
- .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
525
- .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
526
- .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
527
- .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
528
-
529
- /* CONTROLS */
530
- .edit-controls { position: fixed; bottom: 20px; right: 20px; width: 260px; background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 900; font-size: 13px; max-height: 90vh; overflow-y: auto; }
531
- .edit-controls h4 { margin: 0 0 10px 0; color: #4ECDC4; text-align: center; }
532
- .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
533
- .control-group label { font-size: 11px; font-weight: bold; display: block; margin-bottom: 3px; }
534
- button, input, select { width: 100%; margin-top: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; cursor: pointer; font-weight: bold; font-size: 12px; }
535
- .button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
536
- .color-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
537
- .slider-container { display: flex; align-items: center; gap: 5px; margin-top: 5px; }
538
- .slider-container label { min-width: 40px; font-size: 11px; }
539
- .action-btn { background: #4CAF50; color: white; }
540
- .reset-btn { background: #e74c3c; color: white; }
541
- .secondary-btn { background: #f39c12; color: white; }
542
- .export-btn { background: #2196F3; color: white; }
543
- .save-btn { background: #9b59b6; color: white; }
544
- .undo-btn { background: #7f8c8d; color: white; margin-bottom: 5px; }
545
-
546
- /* MODAL */
547
- .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; justify-content: center; align-items: center; z-index: 9999; }
548
- .modal-content { background: white; padding: 30px; border-radius: 12px; max-width: 400px; width: 90%; text-align: center; }
549
- .modal-content .code { font-size: 32px; font-weight: bold; letter-spacing: 4px; background: #f0f0f0; padding: 15px 25px; border-radius: 8px; display: inline-block; margin: 15px 0; font-family: monospace; user-select: all; }
550
- .modal-content button { background: #3498db; color: white; border: none; padding: 12px 30px; border-radius: 8px; cursor: pointer; font-weight: bold; margin: 5px; }
551
  </style>
552
- </head> <body> <div id="upload-container"> <div class="upload-box"> <h1>🎬 Enhanced Comic Generator</h1> <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name"> <label for="file-upload" class="file-label">📁 Choose Video File</label> <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
553
- <div class="page-input-group">
554
- <label>📚 Total Comic Pages:</label>
555
- <input type="number" id="page-count" value="4" min="1" max="15" placeholder="e.g. 4 (Video will be divided evenly)">
556
- <small style="color:#666; font-size:11px; display:block; margin-top:5px;">System calculates 5 panels per page.</small>
557
- </div>
558
-
559
- <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
560
- <button id="restore-draft-btn" class="restore-btn" style="display:none; margin-top:15px;" onclick="restoreDraft()">📂 Restore Unsaved Draft</button>
561
-
562
- <div class="load-section">
563
- <h3>📥 Load Saved Comic</h3>
564
- <div class="load-input-group">
565
- <input type="text" id="load-code-input" placeholder="SAVE CODE" maxlength="8" style="text-transform:uppercase;">
566
- <button onclick="loadSavedComic()">Load</button>
567
- </div>
568
- </div>
569
- <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
570
- <div class="loader" style="margin:0 auto;"></div>
571
- <p id="status-text" style="margin-top:10px;">Starting...</p>
572
- </div>
573
- </div>
574
- </div>
575
- <div id="editor-container">
576
- <div class="comic-wrapper" id="comic-container"></div>
577
- <input type="file" id="image-uploader" style="display: none;" accept="image/*">
578
- <div class="edit-controls">
579
- <h4>✏️ Interactive Editor</h4>
580
-
581
- <button onclick="undoLastAction()" class="undo-btn">↩️ Undo</button>
582
-
583
- <div class="control-group">
584
- <label>💾 Save & Load:</label>
585
- <button onclick="saveComic()" class="save-btn">💾 Save Comic</button>
586
- <div id="current-save-code" style="display:none; margin-top:5px; text-align:center;">
587
- <span id="display-save-code" style="font-weight:bold; background:#eee; padding:2px 5px; border-radius:3px;"></span>
588
- <button onclick="copyCode()" style="padding:2px; width:auto; font-size:10px;">Copy</button>
589
- </div>
590
- </div>
591
-
592
- <div class="control-group">
593
- <label>💬 Bubble Styling:</label>
594
- <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
595
- <option value="speech">Speech</option>
596
- <option value="thought">Thought</option>
597
- <option value="reaction">Reaction (Shout)</option>
598
- <option value="narration">Narration (Box)</option>
599
- </select>
600
- <select id="font-select" onchange="changeFont(this.value)" disabled>
601
- <option value="'Comic Neue', cursive">Comic Neue</option>
602
- <option value="'Bangers', cursive">Bangers</option>
603
- <option value="'Gloria Hallelujah', cursive">Gloria</option>
604
- <option value="'Lato', sans-serif">Lato</option>
605
- </select>
606
- <div class="color-grid">
607
- <div><label>Text</label><input type="color" id="bubble-text-color" value="#ffffff" disabled></div>
608
- <div><label>Fill</label><input type="color" id="bubble-fill-color" value="#4ECDC4" disabled></div>
609
- </div>
610
- <div class="button-grid">
611
- <button onclick="addBubble()" class="action-btn">Add</button>
612
- <button onclick="deleteBubble()" class="reset-btn">Delete</button>
613
- </div>
614
- </div>
615
 
616
- <div class="control-group" id="tail-controls" style="display:none;">
617
- <label>📐 Tail Adjustment:</label>
618
- <button onclick="rotateTail()" class="secondary-btn">🔄 Rotate Side</button>
619
- <div class="slider-container">
620
- <label>Pos:</label>
621
- <input type="range" id="tail-slider" min="10" max="90" value="50" oninput="slideTail(this.value)">
622
- </div>
623
  </div>
624
 
625
- <div class="control-group">
626
- <label>🖼️ Panel Tools:</label>
627
- <button onclick="toggleFitCover()" class="action-btn">🔄 Toggle Fit/Fill</button>
628
- <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
629
- <div class="button-grid">
630
- <button onclick="adjustFrame('backward')" class="secondary-btn" id="prev-btn">⬅️ Prev</button>
631
- <button onclick="adjustFrame('forward')" class="action-btn" id="next-btn">Next ➡️</button>
632
- </div>
633
- <div class="timestamp-controls">
634
- <input type="text" id="timestamp-input" placeholder="mm:ss or secs">
635
- <button onclick="gotoTimestamp()" class="action-btn" id="go-btn">Go</button>
636
- </div>
637
- </div>
638
 
639
- <div class="control-group">
640
- <label>🔍 Zoom & Pan:</label>
641
- <div class="button-grid">
642
- <button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
643
- <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
644
- </div>
645
  </div>
646
 
647
- <div class="control-group">
648
- <button onclick="exportComic()" class="export-btn">📥 Export as PNG</button>
649
- <button onclick="goBackToUpload()" class="reset-btn" style="margin-top:10px;">🏠 Home</button>
650
  </div>
651
  </div>
652
  </div>
653
- <div class="modal-overlay" id="save-modal">
654
- <div class="modal-content">
655
- <h2>✅ Comic Saved!</h2>
656
- <div class="code" id="modal-save-code">XXXXXXXX</div>
657
- <button onclick="copyModalCode()">📋 Copy Code</button>
658
- <button class="close-btn" onclick="closeModal()">Close</button>
 
659
  </div>
 
 
660
  </div>
 
661
  <script>
662
- function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
663
- let sid = localStorage.getItem('comic_sid') || genUUID();
664
- localStorage.setItem('comic_sid', sid);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
- let currentSaveCode = null;
667
- let isProcessing = false;
668
- let interval, selectedBubble = null, selectedPanel = null;
669
- let isDragging = false, isResizing = false, isPanning = false;
670
- let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
671
- let resizeHandle, originalWidth, originalHeight, originalMouseX, originalMouseY;
672
- let currentlyEditing = null;
673
 
674
- // UNDO SYSTEM
675
- let historyStack = [];
676
- let historyIndex = -1;
677
- function addToHistory() {
678
- if (historyIndex < historyStack.length - 1) {
679
- historyStack = historyStack.slice(0, historyIndex + 1);
680
- }
681
- const state = JSON.stringify(getCurrentState());
682
- if (historyStack.length > 0 && historyStack[historyStack.length - 1] === state) return;
683
 
684
- historyStack.push(state);
685
- historyIndex++;
686
 
687
- if (historyStack.length > 30) {
688
- historyStack.shift();
689
- historyIndex--;
690
- }
691
- }
692
- function undoLastAction() {
693
- if (historyIndex > 0) {
694
- historyIndex--;
695
- const previousState = JSON.parse(historyStack[historyIndex]);
696
- renderFromState(previousState);
697
- saveDraft(false);
698
- }
699
- }
700
-
701
- if(localStorage.getItem('comic_draft_'+sid)) document.getElementById('restore-draft-btn').style.display = 'block';
702
-
703
- function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
704
- function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
705
- function copyModalCode() { navigator.clipboard.writeText(document.getElementById('modal-save-code').textContent).then(() => alert('Code copied!')); }
706
- function copyCode() { if(currentSaveCode) navigator.clipboard.writeText(currentSaveCode).then(() => alert('Code copied!')); }
707
-
708
- function setProcessing(busy) {
709
- isProcessing = busy;
710
- const btns = ['prev-btn', 'next-btn', 'go-btn'];
711
- btns.forEach(id => {
712
- const el = document.getElementById(id);
713
- if(el) { el.disabled = busy; el.style.opacity = busy ? '0.5' : '1'; el.innerText = busy ? '⏳' : el.getAttribute('data-txt') || el.innerText; }
714
- });
715
- }
716
- async function saveComic() {
717
- const state = getCurrentState();
718
- if(!state || state.length === 0) { alert('No comic to save!'); return; }
719
- try {
720
- const r = await fetch(`/save_comic?sid=${sid}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ pages: state, savedAt: new Date().toISOString() }) });
721
- const d = await r.json();
722
- if(d.success) { currentSaveCode = d.code; document.getElementById('display-save-code').textContent = d.code; document.getElementById('current-save-code').style.display = 'block'; showSaveModal(d.code); saveDraft(false); }
723
- else { alert('Failed to save: ' + d.message); }
724
- } catch(e) { console.error(e); alert('Error saving comic'); }
725
- }
726
-
727
- async function loadSavedComic() {
728
- const code = document.getElementById('load-code-input').value.trim().toUpperCase();
729
- if(!code || code.length < 4) { alert('Invalid code'); return; }
730
- try {
731
- const r = await fetch(`/load_comic/${code}`);
732
- const d = await r.json();
733
- if(d.success) { currentSaveCode = code; sid = d.originalSid || sid; localStorage.setItem('comic_sid', sid); renderFromState(d.pages); document.getElementById('upload-container').style.display = 'none'; document.getElementById('editor-container').style.display = 'block'; document.getElementById('display-save-code').textContent = code; document.getElementById('current-save-code').style.display = 'block'; saveDraft(true); }
734
- else { alert('Load failed: ' + d.message); }
735
- } catch(e) { console.error(e); alert('Error loading comic.'); }
736
- }
737
-
738
- function restoreDraft() {
739
- try {
740
- const state = JSON.parse(localStorage.getItem('comic_draft_'+sid));
741
- if(state.saveCode) { currentSaveCode = state.saveCode; document.getElementById('display-save-code').textContent = state.saveCode; document.getElementById('current-save-code').style.display = 'block'; }
742
- renderFromState(state.pages || state);
743
- document.getElementById('upload-container').style.display = 'none';
744
- document.getElementById('editor-container').style.display = 'block';
745
- addToHistory();
746
- } catch(e) { console.error(e); alert("Failed to restore."); }
747
- }
748
-
749
- function getCurrentState() {
750
- const pages = [];
751
- document.querySelectorAll('.comic-page').forEach(p => {
752
- const panels = [];
753
- const grid = p.querySelector('.comic-grid');
754
- grid.querySelectorAll('.panel').forEach(pan => {
755
- const img = pan.querySelector('img');
756
- const bubbles = [];
757
- pan.querySelectorAll('.speech-bubble').forEach(b => {
758
- const textEl = b.querySelector('.bubble-text');
759
- bubbles.push({
760
- text: textEl ? textEl.textContent : '',
761
- left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height,
762
- classes: b.className.replace(' selected', ''),
763
- type: b.dataset.type, font: b.style.fontFamily,
764
- tailPos: b.style.getPropertyValue('--tail-pos'),
765
- colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
766
- });
767
- });
768
- panels.push({
769
- src: img.src,
770
- zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
771
- fit: img.style.objectFit,
772
- bubbles: bubbles
773
- });
774
- });
775
- pages.push({ panels: panels });
776
- });
777
- return pages;
778
- }
779
-
780
- function saveDraft(recordHistory = true) {
781
- if(recordHistory) addToHistory();
782
- localStorage.setItem('comic_draft_'+sid, JSON.stringify({ pages: getCurrentState(), saveCode: currentSaveCode, savedAt: new Date().toISOString() }));
783
- }
784
-
785
- function renderFromState(pagesData) {
786
- const con = document.getElementById('comic-container'); con.innerHTML = '';
787
- pagesData.forEach((page, pageIdx) => {
788
- const pageWrapper = document.createElement('div'); pageWrapper.className = 'page-wrapper';
789
- const pageTitle = document.createElement('h2'); pageTitle.className = 'page-title'; pageTitle.textContent = `Page ${pageIdx + 1}`;
790
- pageWrapper.appendChild(pageTitle);
791
 
792
- const div = document.createElement('div'); div.className = 'comic-page';
793
- const grid = document.createElement('div'); grid.className = 'comic-grid';
794
 
795
- page.panels.forEach((pan) => {
796
- const pDiv = document.createElement('div'); pDiv.className = 'panel';
797
- pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
798
- const img = document.createElement('img');
799
- img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
800
- img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
801
-
802
- // RESTORE FIT STATE
803
- img.style.objectFit = pan.fit || 'contain'; // Default to 'contain' to avoid initial crop
804
-
805
- updateImageTransform(img);
806
- img.onmousedown = (e) => startPan(e, img);
807
- pDiv.appendChild(img);
808
- (pan.bubbles || []).forEach(bData => { pDiv.appendChild(createBubbleHTML(bData)); });
809
- grid.appendChild(pDiv);
810
- });
811
- div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
812
- });
813
- selectedBubble = null;
814
- selectedPanel = null;
815
- document.getElementById('bubble-type-select').disabled = true;
816
- document.getElementById('font-select').disabled = true;
817
- }
818
-
819
- async function upload() {
820
- const f = document.getElementById('file-upload').files[0];
821
- const pCount = document.getElementById('page-count').value;
822
- if(!f) return alert("Select a video");
823
- sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
824
- document.querySelector('.upload-box').style.display='none';
825
- document.getElementById('loading-view').style.display='flex';
826
- const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount);
827
- const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
828
- if(r.ok) interval = setInterval(checkStatus, 2000);
829
- else { alert("Upload failed"); location.reload(); }
830
- }
831
-
832
- async function checkStatus() {
833
- try {
834
- const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
835
- document.getElementById('status-text').innerText = d.message;
836
- if(d.progress >= 100) { clearInterval(interval); document.getElementById('upload-container').style.display='none'; document.getElementById('editor-container').style.display='block'; loadNewComic(); }
837
- else if (d.progress < 0) { clearInterval(interval); document.getElementById('status-text').textContent = "Error: " + d.message; document.querySelector('.loader').style.display = 'none'; }
838
- } catch(e) {}
839
- }
840
-
841
- function loadNewComic() {
842
- fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
843
- const cleanData = data.map((p, pi) => ({
844
- panels: p.panels.map((pan, j) => ({
845
- src: `/frames/${pan.image}?sid=${sid}`,
846
- bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
847
- text: p.bubbles[j].dialog,
848
- left: (p.bubbles[j].bubble_offset_x || 50) + 'px',
849
- top: (p.bubbles[j].bubble_offset_y || 20) + 'px',
850
- type: (p.bubbles[j].type || 'speech'),
851
- classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`
852
- }] : []
853
- }))
854
- }));
855
- renderFromState(cleanData); saveDraft(true);
856
  });
857
- }
858
-
859
- function createBubbleHTML(data) {
860
- const b = document.createElement('div');
861
- const type = data.type || 'speech';
862
- b.className = data.classes || `speech-bubble ${type} tail-bottom`;
863
- if (type === 'thought' && !b.className.includes('pos-')) b.className += ' pos-bl';
864
-
865
- b.dataset.type = type;
866
- b.style.left = data.left; b.style.top = data.top;
867
- if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
868
- if(data.font) b.style.fontFamily = data.font;
869
- if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
870
- if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
871
 
872
- const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
873
-
874
- if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
875
-
876
- ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
877
-
878
- b.onmousedown = (e) => {
879
- if(e.target.classList.contains('resize-handle')) return;
880
- e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
881
- };
882
- b.onclick = (e) => { e.stopPropagation(); };
883
- b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
884
- return b;
885
- }
886
-
887
- function editBubbleText(bubble) {
888
- if (currentlyEditing) return; currentlyEditing = bubble;
889
- const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea');
890
- textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus();
891
- const finishEditing = () => {
892
- textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null;
893
- saveDraft(true);
894
- };
895
- textarea.addEventListener('blur', finishEditing, { once: true });
896
- textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
897
- }
898
-
899
- document.addEventListener('mousemove', (e) => {
900
- if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
901
- if(isResizing && selectedBubble) { resizeBubble(e); }
902
- // No panning when object-fit is contain (unless zoomed manually via slider)
903
- if(isPanning && selectedPanel) { panImage(e); }
904
- });
905
-
906
- document.addEventListener('mouseup', () => {
907
- if(isDragging || isResizing || isPanning) {
908
- saveDraft(true);
909
- }
910
- isDragging = false; isResizing = false; isPanning = false;
911
  });
912
-
913
- function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
914
- function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
915
-
916
- function selectBubble(el) {
917
- if(selectedBubble) selectedBubble.classList.remove('selected');
918
- if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
919
- selectedBubble = el; el.classList.add('selected');
920
- document.getElementById('bubble-type-select').disabled = false;
921
- document.getElementById('font-select').disabled = false;
922
- document.getElementById('bubble-text-color').disabled = false;
923
- document.getElementById('bubble-fill-color').disabled = false;
924
- document.getElementById('tail-controls').style.display = 'block';
925
- document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
926
- }
927
-
928
- function selectPanel(el) {
929
- if(selectedPanel) selectedPanel.classList.remove('selected');
930
- if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
931
- selectedPanel = el; el.classList.add('selected');
932
- document.getElementById('zoom-slider').disabled = false;
933
- const img = el.querySelector('img');
934
- document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
935
- document.getElementById('bubble-type-select').disabled = true;
936
- document.getElementById('font-select').disabled = true;
937
- document.getElementById('tail-controls').style.display = 'none';
938
- }
939
-
940
- function addBubble() {
941
- if(!selectedPanel) return alert("Select a panel first");
942
- const b = createBubbleHTML({ text: "Text", left: "50px", top: "30px", type: 'speech', classes: "speech-bubble speech tail-bottom" });
943
- selectedPanel.appendChild(b); selectBubble(b); saveDraft(true);
944
- }
945
-
946
- function deleteBubble() {
947
- if(!selectedBubble) return alert("Select a bubble");
948
- selectedBubble.remove(); selectedBubble=null; saveDraft(true);
949
- }
950
-
951
- function changeBubbleType(type) {
952
- if(!selectedBubble) return;
953
- selectedBubble.dataset.type = type;
954
- selectedBubble.className = 'speech-bubble ' + type + ' selected';
955
-
956
- if(type === 'thought') selectedBubble.classList.add('pos-bl');
957
- else selectedBubble.classList.add('tail-bottom');
958
-
959
- selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
960
- if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } }
961
- saveDraft(true);
962
- }
963
-
964
- function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(true); }
965
-
966
- function rotateTail() {
967
- if(!selectedBubble) return;
968
- const type = selectedBubble.dataset.type;
969
-
970
- if(type === 'speech') {
971
- const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left'];
972
- let current = 0;
973
- positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
974
- selectedBubble.classList.remove(positions[current]);
975
- selectedBubble.classList.add(positions[(current + 1) % 4]);
976
- }
977
- else if (type === 'thought') {
978
- const positions = ['pos-bl', 'pos-br', 'pos-tr', 'pos-tl'];
979
- let current = 0;
980
- positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
981
- selectedBubble.classList.remove(positions[current]);
982
- selectedBubble.classList.add(positions[(current + 1) % 4]);
983
- }
984
- saveDraft(true);
985
- }
986
-
987
- function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
988
-
989
- document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
990
- document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
991
-
992
- function handleZoom(el) {
993
- if(!selectedPanel) return;
994
- const img = selectedPanel.querySelector('img');
995
- img.dataset.zoom = el.value;
996
- // Only apply transform:scale if zoom > 100 and object-fit is NOT contain/fill
997
- if(parseFloat(el.value) > 100 && img.style.objectFit !== 'contain' && img.style.objectFit !== 'fill') {
998
- updateImageTransform(img);
999
- } else if (parseFloat(el.value) === 100) {
1000
- img.style.transform = 'none'; // Reset transform if zoom is 100
1001
- img.dataset.translateX = 0; // Reset pan as well
1002
- img.dataset.translateY = 0;
1003
- }
1004
- }
1005
- document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
1006
- function startPan(e, img) {
1007
- // Only allow pan if image is 'cover' or zoomed in
1008
- if((img.style.objectFit === 'contain' && parseFloat(img.dataset.zoom || 100) <= 100) || img.style.objectFit === 'fill') return;
1009
- e.preventDefault();
1010
- isPanning = true;
1011
- selectedPanel = img.closest('.panel');
1012
- panStartX = e.clientX;
1013
- panStartY = e.clientY;
1014
- panStartTx = parseFloat(img.dataset.translateX || 0);
1015
- panStartTy = parseFloat(img.dataset.translateY || 0);
1016
- img.classList.add('panning');
1017
- }
1018
- function panImage(e) {
1019
- if(!isPanning || !selectedPanel) return;
1020
- const img = selectedPanel.querySelector('img');
1021
- img.dataset.translateX = panStartTx + (e.clientX - panStartX);
1022
- img.dataset.translateY = panStartTy + (e.clientY - panStartY);
1023
- updateImageTransform(img);
1024
- }
1025
- function updateImageTransform(img) {
1026
- // THIS FUNCTION IS NOW MODIFIED TO ONLY APPLY TRANSFORM IF NECESSARY
1027
- const z = (img.dataset.zoom || 100) / 100;
1028
- const x = img.dataset.translateX || 0;
1029
- const y = img.dataset.translateY || 0;
1030
-
1031
- // If zoom is 100 and object-fit is 'contain' or 'fill', don't apply transform
1032
- if (z === 1 && (img.style.objectFit === 'contain' || img.style.objectFit === 'fill')) {
1033
- img.style.transform = 'none';
1034
- } else {
1035
- img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
1036
- }
1037
- img.classList.toggle('pannable', z > 1 || img.style.objectFit === 'cover');
1038
- }
1039
- function resetPanelTransform() {
1040
- if(!selectedPanel) return alert("Select a panel");
1041
- const img = selectedPanel.querySelector('img');
1042
- img.dataset.zoom = 100;
1043
- img.dataset.translateX = 0;
1044
- img.dataset.translateY = 0;
1045
- document.getElementById('zoom-slider').value = 100;
1046
- updateImageTransform(img);
1047
- saveDraft(true);
1048
- }
1049
-
1050
- function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
1051
- async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
1052
- async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
1053
-
1054
- // NEW: Toggle between 'contain' (no crop/zoom, but bars), 'fill' (stretch, no bars), and 'cover' (crop, no bars)
1055
- function toggleFitCover() {
1056
- if(!selectedPanel) return alert("Select a panel");
1057
- const img = selectedPanel.querySelector('img');
1058
- const current = img.style.objectFit || 'contain'; // Default if not set
1059
- if(current === 'contain') {
1060
- img.style.objectFit = 'fill'; // Stretch to fill
1061
- } else if(current === 'fill') {
1062
- img.style.objectFit = 'cover'; // Fill, but crop to maintain aspect ratio
1063
- } else { // current === 'cover'
1064
- img.style.objectFit = 'contain'; // Show full image
1065
- }
1066
- // Reset transforms when toggling object-fit to avoid weird interactions
1067
- img.dataset.zoom = 100;
1068
- img.dataset.translateX = 0;
1069
- img.dataset.translateY = 0;
1070
- document.getElementById('zoom-slider').value = 100;
1071
- updateImageTransform(img); // Re-evaluate transform based on new object-fit
1072
- saveDraft(true);
1073
- }
1074
 
1075
- async function exportComic() {
1076
- const pgs = document.querySelectorAll('.comic-page');
1077
- if(pgs.length === 0) return alert("No pages found");
1078
-
1079
- // Remove selection highlights
1080
- if(selectedBubble) selectedBubble.classList.remove('selected');
1081
- if(selectedPanel) selectedPanel.classList.remove('selected');
1082
- alert(`Exporting ${pgs.length} page(s)...`);
1083
-
1084
- // --- PREPARE FOR EXPORT: Remove editor scaling ---
1085
- const comicPages = document.querySelectorAll('.comic-page');
1086
- comicPages.forEach(page => {
1087
- page.style.transform = 'none'; // Remove scaling from comic-page
1088
- page.style.marginBottom = '0'; // Remove margin compensation
1089
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1090
 
1091
- // --- 0% ERROR FIX ---
1092
- // 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
1093
- const bubbles = document.querySelectorAll('.speech-bubble');
1094
- bubbles.forEach(b => {
1095
- const rect = b.getBoundingClientRect();
1096
- // Add slight buffer (1px) to width to handle sub-pixel rendering differences
1097
- // This prevents "just fitting" words from wrapping in the export
1098
- b.style.width = (rect.width + 1) + 'px';
1099
- b.style.height = rect.height + 'px';
1100
- b.style.display = 'flex';
1101
- b.style.alignItems = 'center';
1102
- b.style.justifyContent = 'center';
1103
- });
1104
-
1105
- for(let i = 0; i < pgs.length; i++) {
1106
- try {
1107
- const u = await htmlToImage.toPng(pgs[i], {
1108
- pixelRatio: 2, // High quality for print
1109
- style: { transform: 'none' } // Ensure no transforms for html-to-image
1110
- });
1111
- const a = document.createElement('a');
1112
- a.href = u;
1113
- a.download = `Comic-Page-${i+1}.png`;
1114
- a.click();
1115
- } catch(err) {
1116
- console.error(err);
1117
- alert(`Failed to export page ${i+1}`);
1118
- }
1119
- }
1120
-
1121
- // --- RESTORE EDITOR SCALING AFTER EXPORT ---
1122
- comicPages.forEach(page => {
1123
- page.style.transform = 'scale(0.6)';
1124
- page.style.marginBottom = '-280px';
1125
- });
1126
- }
1127
-
1128
- function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
1129
  </script>
1130
- </body> </html> '''
 
 
1131
 
1132
  @app.route('/')
1133
  def index():
 
328
  # 🌐 ROUTES & FULL UI
329
  # ======================================================
330
  INDEX_HTML = '''
331
+ <!DOCTYPE html>
332
+ <html lang="en">
333
+ <head>
334
+ <meta charset="UTF-8">
335
+ <title>CineComic Editor — True Size</title>
336
+ <meta name="viewport" content="width=device-width, initial-scale=1">
337
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
338
+ <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
339
+
340
+ <style>
341
+ /* =====================================================
342
+ GLOBAL RESET — ABSOLUTE BASELINE
343
+ ===================================================== */
344
+ * {
345
+ margin: 0;
346
+ padding: 0;
347
+ box-sizing: border-box;
348
+ }
349
+
350
+ html, body {
351
+ background: #f0f0f0;
352
+ font-family: system-ui, Arial, sans-serif;
353
+ }
354
+
355
+ /* =====================================================
356
+ PAGE WRAPPER (NO SCALE, EVER)
357
+ ===================================================== */
358
+ #workspace {
359
+ padding: 20px;
360
+ display: flex;
361
+ flex-direction: column;
362
+ align-items: center;
363
+ }
364
+
365
+ /* =====================================================
366
+ COMIC PAGE TRUE RESOLUTION
367
+ 1000px Width x 712px Height
368
+ ===================================================== */
369
+ .comic-page {
370
+ width: 1000px;
371
+ height: 712px;
372
+ margin: 0 auto 40px auto;
373
+ background: #fff;
374
+ border: 2px solid #000;
375
+ position: relative;
376
+ overflow: hidden;
377
+ box-shadow: 0 5px 25px rgba(0,0,0,0.1);
378
+ }
379
+
380
+ .page-title {
381
+ font-size: 24px;
382
+ font-weight: bold;
383
+ margin-bottom: 10px;
384
+ color: #333;
385
+ }
386
+
387
+ /* =====================================================
388
+ PANELS 5 PANEL SLANTED LAYOUT
389
+ ===================================================== */
390
+ .panel {
391
+ position: absolute;
392
+ /* Removed border to allow gutters to be pure white */
393
+ /* border: 2px solid #000; */
394
+ overflow: hidden;
395
+ background: #fff; /* White background for gaps */
396
+ }
397
+
398
+ /*
399
+ Reference: 1000px width x 712px height.
400
+ Gutter: 12px horizontal (1.2%), 12px vertical (1.7%).
401
+ Half Gutter: 0.6% Horz, 0.85% Vert.
402
+
403
+ COORDINATES from Green Text:
404
+ Row 1 Split: Top=63.2%, Bottom=59.5%
405
+ Row 2 Left Split: Top=33.0%, Bottom=35.7%
406
+ Row 2 Right Split: Top=64.8%, Bottom=68.2%
407
+ Tier Height: 350px (approx 49.15% of 712px)
408
+ */
409
+
410
+ /* --- ROW 1 (TOP) --- Height 49.15% */
411
+
412
+ /* Panel 1: Right ends at (63.2 - 0.6)% / (59.5 - 0.6)% */
413
+ .panel:nth-child(1) {
414
+ top: 0; left: 0; height: 49.15%; width: 100%;
415
+ clip-path: polygon(0% 0%, 62.6% 0%, 58.9% 100%, 0% 100%);
416
+ }
417
+ /* Panel 2: Left starts at (63.2 + 0.6)% / (59.5 + 0.6)% */
418
+ .panel:nth-child(2) {
419
+ top: 0; left: 0; height: 49.15%; width: 100%;
420
+ clip-path: polygon(63.8% 0%, 100% 0%, 100% 100%, 60.1% 100%);
421
+ }
422
+
423
+ /* --- ROW 2 (BOTTOM) --- Top: 50.85% (Gap ~1.7%), Height: 49.15% */
424
+
425
+ /* Panel 3: Right ends at (33.0 - 0.6)% / (35.7 - 0.6)% */
426
+ .panel:nth-child(3) {
427
+ top: 50.85%; left: 0; height: 49.15%; width: 100%;
428
+ clip-path: polygon(0% 0%, 32.4% 0%, 35.1% 100%, 0% 100%);
429
+ }
430
+ /* Panel 4: Left starts (33.0 + 0.6)% / (35.7 + 0.6)%. Right ends (64.8 - 0.6)% / (68.2 - 0.6)% */
431
+ .panel:nth-child(4) {
432
+ top: 50.85%; left: 0; height: 49.15%; width: 100%;
433
+ clip-path: polygon(33.6% 0%, 64.2% 0%, 67.6% 100%, 36.3% 100%);
434
+ }
435
+ /* Panel 5: Left starts (64.8 + 0.6)% / (68.2 + 0.6)% */
436
+ .panel:nth-child(5) {
437
+ top: 50.85%; left: 0; height: 49.15%; width: 100%;
438
+ clip-path: polygon(65.4% 0%, 100% 0%, 100% 100%, 68.8% 100%);
439
+ }
440
+
441
+ /* =====================================================
442
+ PANEL IMAGES — FINAL RULES
443
+ ===================================================== */
444
+ .panel img {
445
+ width: 100%;
446
+ height: 100%;
447
+ object-fit: contain; /* 🔒 NEVER CROP */
448
+ transform: none !important; /* 🔒 NEVER SCALE */
449
+ display: block;
450
+ background: #fff; /* White bars if ratio mismatch */
451
+ image-rendering: auto;
452
+ }
453
+
454
+ /* =====================================================
455
+ SPEECH / THOUGHT BUBBLES
456
+ ===================================================== */
457
+ .bubble, .speech-bubble {
458
+ position: absolute;
459
+ max-width: 260px;
460
+ background: #4ECDC4;
461
+ color: white;
462
+ border: 2px solid transparent;
463
+ border-radius: 18px;
464
+ padding: 10px 14px;
465
+ font-size: 14px;
466
+ line-height: 1.35;
467
+ cursor: move;
468
+ z-index: 100;
469
+ font-family: 'Comic Neue', cursive;
470
+ font-weight: bold;
471
+ text-align: center;
472
+ }
473
+
474
+ /* Tail logic handled via JS classes in previous logic, simplifying here for "True Size" */
475
+ .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 101; }
476
+
477
+ /* =====================================================
478
+ TOOLBAR & UI
479
+ ===================================================== */
480
+ .toolbar {
481
+ width: 1000px;
482
+ margin: 0 auto 20px auto;
483
+ display: flex;
484
+ gap: 10px;
485
+ background: white;
486
+ padding: 10px;
487
+ border-radius: 8px;
488
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
489
+ justify-content: center;
490
+ }
491
+
492
+ .toolbar button {
493
+ padding: 8px 16px;
494
+ border: 1px solid #ddd;
495
+ background: #fff;
496
+ cursor: pointer;
497
+ font-weight: bold;
498
+ border-radius: 4px;
499
+ }
500
+
501
+ .toolbar button:hover {
502
+ background: #f0f0f0;
503
+ }
504
+
505
+ #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
506
+ .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
507
+ .file-input { display: none; }
508
+ .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; }
509
+ .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top:10px; }
510
+ .loader { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 20px auto; }
511
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  </style>
514
+ </head>
515
+
516
+ <body>
517
+
518
+ <!-- UPLOAD SECTION -->
519
+ <div id="upload-container">
520
+ <div class="upload-box">
521
+ <h1>🎬 CineComic Editor</h1>
522
+ <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name">
523
+ <label for="file-upload" class="file-label">📁 Choose Video File</label>
524
+ <span id="fn" style="display:block; margin-bottom:10px; color:#666;">No file selected</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
526
+ <div style="margin: 20px 0; text-align:left;">
527
+ <label style="font-weight:bold; display:block; margin-bottom:5px;">📚 Pages:</label>
528
+ <input type="number" id="page-count" value="4" min="1" max="15" style="width:100%; padding:10px; border-radius:5px; border:1px solid #ccc;">
 
 
 
 
529
  </div>
530
 
531
+ <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
+ <div id="loading-view" style="display:none; margin-top:20px;">
534
+ <div class="loader"></div>
535
+ <p id="status-text">Processing...</p>
 
 
 
536
  </div>
537
 
538
+ <div style="margin-top:20px; border-top:1px solid #eee; padding-top:20px;">
539
+ <input type="text" id="load-code-input" placeholder="SAVE CODE" maxlength="8" style="padding:10px; width:150px; text-align:center; text-transform:uppercase;">
540
+ <button onclick="loadSavedComic()" style="padding:10px 20px; cursor:pointer;">Load</button>
541
  </div>
542
  </div>
543
  </div>
544
+
545
+ <!-- EDITOR WORKSPACE -->
546
+ <div id="workspace" style="display:none;">
547
+ <div class="toolbar">
548
+ <button onclick="saveComic()">💾 Save</button>
549
+ <button onclick="exportComic()">📥 Export PNG</button>
550
+ <button onclick="location.reload()">🏠 Home</button>
551
  </div>
552
+
553
+ <div id="comic-container"></div>
554
  </div>
555
+
556
  <script>
557
+ /* =====================================================
558
+ LOGIC & API
559
+ ===================================================== */
560
+ function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
561
+ let sid = localStorage.getItem('comic_sid') || genUUID();
562
+ localStorage.setItem('comic_sid', sid);
563
+ let interval;
564
+
565
+ // --- GLOBAL IMAGE POLICY ---
566
+ function forceImageRules(img) {
567
+ img.style.objectFit = 'contain';
568
+ img.style.transform = 'none';
569
+ img.style.width = '100%';
570
+ img.style.height = '100%';
571
+ img.style.display = 'block';
572
+ img.removeAttribute('data-zoom');
573
+ }
574
+
575
+ // --- UPLOAD ---
576
+ async function upload() {
577
+ const f = document.getElementById('file-upload').files[0];
578
+ const pCount = document.getElementById('page-count').value;
579
+ if(!f) return alert("Select a video");
580
+
581
+ sid = genUUID(); localStorage.setItem('comic_sid', sid);
582
+ document.querySelector('.upload-box').style.display='none';
583
+ document.getElementById('loading-view').style.display='block';
584
+
585
+ const fd = new FormData();
586
+ fd.append('file', f);
587
+ fd.append('target_pages', pCount);
588
+
589
+ const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
590
+ if(r.ok) interval = setInterval(checkStatus, 2000);
591
+ else { alert("Upload failed"); location.reload(); }
592
+ }
593
+
594
+ async function checkStatus() {
595
+ try {
596
+ const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
597
+ document.getElementById('status-text').innerText = d.message;
598
+ if(d.progress >= 100) {
599
+ clearInterval(interval);
600
+ document.getElementById('upload-container').style.display='none';
601
+ document.getElementById('workspace').style.display='flex';
602
+ loadNewComic();
603
+ }
604
+ } catch(e) {}
605
+ }
606
+
607
+ function loadNewComic() {
608
+ fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
609
+ renderComic(data);
610
+ });
611
+ }
612
+
613
+ function renderComic(data) {
614
+ const container = document.getElementById('comic-container');
615
+ container.innerHTML = '';
616
 
617
+ // Transform backend data format if needed, assume it matches expected 'panels'
618
+ // Backend returns list of pages, each with 'panels' array
 
 
 
 
 
619
 
620
+ data.forEach((pageData, idx) => {
621
+ // Wrapper for title
622
+ const wrapper = document.createElement('div');
623
+ const title = document.createElement('div');
624
+ title.className = 'page-title';
625
+ title.innerText = `Page ${idx + 1}`;
626
+ wrapper.appendChild(title);
 
 
627
 
628
+ const pageDiv = document.createElement('div');
629
+ pageDiv.className = 'comic-page';
630
 
631
+ // Render 5 panels strictly
632
+ pageData.panels.forEach((p, i) => {
633
+ if(i >= 5) return; // Only template supports 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
+ const panelDiv = document.createElement('div');
636
+ panelDiv.className = 'panel';
637
 
638
+ const img = document.createElement('img');
639
+ img.src = `/frames/${p.image}?sid=${sid}`;
640
+ forceImageRules(img); // APPLY TRUE SIZE RULES
641
+
642
+ panelDiv.appendChild(img);
643
+
644
+ // Add bubbles if any
645
+ if(pageData.bubbles && pageData.bubbles[i]) {
646
+ const bData = pageData.bubbles[i];
647
+ if(bData.dialog) {
648
+ const bubble = document.createElement('div');
649
+ bubble.className = `speech-bubble ${bData.type || 'speech'}`;
650
+ bubble.style.left = (bData.bubble_offset_x || 50) + 'px';
651
+ bubble.style.top = (bData.bubble_offset_y || 20) + 'px';
652
+ bubble.innerText = bData.dialog;
653
+
654
+ // Simple drag logic for bubbles
655
+ bubble.onmousedown = function(e) {
656
+ e.stopPropagation();
657
+ let shiftX = e.clientX - bubble.getBoundingClientRect().left;
658
+ let shiftY = e.clientY - bubble.getBoundingClientRect().top;
659
+
660
+ function moveAt(pageX, pageY) {
661
+ // Relative to pageDiv
662
+ let rect = pageDiv.getBoundingClientRect();
663
+ bubble.style.left = pageX - shiftX - rect.left + 'px';
664
+ bubble.style.top = pageY - shiftY - rect.top + 'px';
665
+ }
666
+
667
+ function onMouseMove(event) { moveAt(event.pageX, event.pageY); }
668
+ document.addEventListener('mousemove', onMouseMove);
669
+ bubble.onmouseup = function() {
670
+ document.removeEventListener('mousemove', onMouseMove);
671
+ bubble.onmouseup = null;
672
+ };
673
+ };
674
+
675
+ panelDiv.appendChild(bubble);
676
+ }
677
+ }
678
+
679
+ pageDiv.appendChild(panelDiv);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
+ wrapper.appendChild(pageDiv);
683
+ container.appendChild(wrapper);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  });
685
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
 
687
+ // --- EXPORT ---
688
+ async function exportComic() {
689
+ const pgs = document.querySelectorAll('.comic-page');
690
+ for(let i=0; i<pgs.length; i++) {
691
+ try {
692
+ const u = await htmlToImage.toPng(pgs[i], { pixelRatio: 2 });
693
+ const a = document.createElement('a'); a.href = u; a.download = `Page-${i+1}.png`; a.click();
694
+ } catch(e) { console.error(e); }
695
+ }
696
+ }
697
+
698
+ // --- SAVE / LOAD SKELETON ---
699
+ async function saveComic() {
700
+ // Logic similar to previous but simplified data structure
701
+ alert("Save implemented on backend, frontend state gathering required.");
702
+ }
703
+ async function loadSavedComic() {
704
+ const code = document.getElementById('load-code-input').value;
705
+ const r = await fetch(`/load_comic/${code}`);
706
+ const d = await r.json();
707
+ if(d.success) {
708
+ sid = d.originalSid;
709
+ document.getElementById('upload-container').style.display='none';
710
+ document.getElementById('workspace').style.display='flex';
711
+ // Need to adapt saved data structure to renderComic
712
+ // renderComic(d.pages);
713
+ alert("Loaded! (Rendering logic needs full state match)");
714
+ location.reload(); // Quick fix for demo to reload state from server files
715
+ }
716
+ }
717
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  </script>
719
+ </body>
720
+ </html>
721
+ '''
722
 
723
  @app.route('/')
724
  def index():