tester343 commited on
Commit
7bbdd4f
·
verified ·
1 Parent(s): 8e724fa

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +171 -94
app_enhanced.py CHANGED
@@ -118,8 +118,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
118
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
119
 
120
  if target_pages <= 0: target_pages = 1
121
- # UPDATED: 5 Panels per page for the new polygon layout
122
- panels_per_page = 5
123
  total_panels_needed = target_pages * panels_per_page
124
 
125
  selected_moments = []
@@ -177,16 +176,12 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
177
  elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
178
  elif '?' in dialogue: b_type = 'speech'
179
 
180
- try:
181
- faces = face_detector.detect_faces(p)
182
- lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
183
- # Adjust placement for new panel sizes?
184
- # The AI placement is generic, we let the user adjust in UI
185
- bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
186
- b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1], type=b_type)
187
- bubbles_list.append(b)
188
- except:
189
- bubbles_list.append(bubble(dialog=dialogue, type=b_type))
190
 
191
  pages = []
192
  for i in range(target_pages):
@@ -336,41 +331,41 @@ INDEX_HTML = '''
336
  /* ========================================= */
337
  /* 🎨 NEW POLYGON TEMPLATE LAYOUT CSS */
338
  /* ========================================= */
339
-
340
- /*
341
- Total Width: 1000px
342
- Tier Height: 350px
343
- Gutter: 10px
344
-
345
- Row 1 Y: 0 - 350
346
- Row 2 Y: 360 - 710
347
- Total Height: 710px
348
- */
349
 
350
  .comic-wrapper { max-width: 1050px; margin: 0 auto; }
351
  .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
352
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
353
 
 
 
 
 
 
 
 
354
  .comic-page {
355
  background: white;
356
  width: 1000px;
357
  height: 710px;
358
  box-shadow: 0 4px 10px rgba(0,0,0,0.1);
359
  position: relative;
360
- overflow: hidden;
361
- border: none;
362
  }
363
 
364
- /* We use absolute positioning for panels with clip-path */
 
 
 
 
365
  .panel {
366
  position: absolute;
367
  background: #eee;
368
  cursor: pointer;
369
- overflow: hidden;
370
- border: none;
371
  }
372
-
373
- .panel.selected { filter: brightness(0.9) sepia(0.2); z-index: 5; }
374
 
375
  .panel img {
376
  width: 100%;
@@ -382,54 +377,71 @@ INDEX_HTML = '''
382
  .panel img.pannable { cursor: grab; }
383
  .panel img.panning { cursor: grabbing; }
384
 
385
- /*
386
- Coordinates Logic with 10px Gutter (approx +/- 5px from center lines)
387
-
388
- Row 1 Divider: (635.2, 0) to (588.2, 350)
389
- Row 2 Left Div: (293.2, 0) to (326.2, 350)
390
- Row 2 Right Div: (617.2, 0) to (666.2, 350)
391
-
392
- Row 1 Top: 0px. Row 1 Height: 350px.
393
- Row 2 Top: 360px. Row 2 Height: 350px.
394
  */
395
-
396
- /* --- TIER 1 --- */
397
- /* Panel 0 (Top Left) */
398
  .panel-0 {
399
- top: 0; left: 0; width: 1000px; height: 350px;
400
- clip-path: polygon(0% 0%, 630.2px 0%, 583.2px 100%, 0% 100%);
401
  }
402
 
403
- /* Panel 1 (Top Right) */
 
 
 
 
 
 
 
 
 
404
  .panel-1 {
405
- top: 0; left: 0; width: 1000px; height: 350px;
406
- clip-path: polygon(640.2px 0%, 1000px 0%, 1000px 100%, 593.2px 100%);
407
  }
408
 
409
- /* --- TIER 2 --- */
410
- /* Panel 2 (Bottom Left) */
 
 
 
 
 
411
  .panel-2 {
412
- top: 360px; left: 0; width: 1000px; height: 350px;
413
- clip-path: polygon(0% 0%, 288.2px 0%, 321.2px 100%, 0% 100%);
414
  }
415
 
416
- /* Panel 3 (Bottom Middle) */
 
 
 
 
417
  .panel-3 {
418
- top: 360px; left: 0; width: 1000px; height: 350px;
419
- clip-path: polygon(298.2px 0%, 612.2px 0%, 661.2px 100%, 331.2px 100%);
420
  }
421
 
422
- /* Panel 4 (Bottom Right) */
 
 
 
423
  .panel-4 {
424
- top: 360px; left: 0; width: 1000px; height: 350px;
425
- clip-path: polygon(622.2px 0%, 1000px 0%, 1000px 100%, 671.2px 100%);
426
  }
427
 
428
  /* SPEECH BUBBLES */
 
429
  .speech-bubble {
430
  position: absolute; display: flex; justify-content: center; align-items: center;
431
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
432
- z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
 
433
  font-size: 13px; text-align: center;
434
  overflow: visible;
435
  line-height: 1.2;
@@ -453,7 +465,7 @@ INDEX_HTML = '''
453
  border-radius: inherit;
454
  }
455
 
456
- .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
457
  .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; }
458
 
459
  /* SPEECH BUBBLE CSS (Tails) */
@@ -722,31 +734,44 @@ INDEX_HTML = '''
722
  } catch(e) { console.error(e); alert("Failed to restore."); }
723
  }
724
 
 
 
 
 
 
 
 
 
 
 
725
  function getCurrentState() {
726
  const pages = [];
727
  document.querySelectorAll('.comic-page').forEach(p => {
728
  const panels = [];
 
 
729
  p.querySelectorAll('.panel').forEach(pan => {
730
  const img = pan.querySelector('img');
731
- const bubbles = [];
732
- pan.querySelectorAll('.speech-bubble').forEach(b => {
733
- const textEl = b.querySelector('.bubble-text');
734
- bubbles.push({
735
- text: textEl ? textEl.textContent : '',
736
- left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height,
737
- classes: b.className.replace(' selected', ''),
738
- type: b.dataset.type, font: b.style.fontFamily,
739
- tailPos: b.style.getPropertyValue('--tail-pos'),
740
- colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
741
- });
742
- });
743
  panels.push({
744
  src: img.src,
745
  zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
746
- bubbles: bubbles
 
 
 
 
 
 
 
 
 
 
 
 
747
  });
748
  });
749
- pages.push({ panels: panels });
 
750
  });
751
  return pages;
752
  }
@@ -764,12 +789,11 @@ INDEX_HTML = '''
764
  pageWrapper.appendChild(pageTitle);
765
 
766
  const div = document.createElement('div');
767
- div.className = 'comic-page'; // Now 1000x710px
768
 
769
- // Render 5 panels using specific layout classes
770
  page.panels.forEach((pan, idx) => {
771
  const pDiv = document.createElement('div');
772
- // Cycle through 0-4 for layout positions if there are more panels, or just use idx
773
  const posClass = `panel-${idx % 5}`;
774
  pDiv.className = `panel ${posClass}`;
775
 
@@ -780,10 +804,33 @@ INDEX_HTML = '''
780
  updateImageTransform(img);
781
  img.onmousedown = (e) => startPan(e, img);
782
  pDiv.appendChild(img);
783
- (pan.bubbles || []).forEach(bData => { pDiv.appendChild(createBubbleHTML(bData)); });
 
 
 
 
 
 
 
 
784
  div.appendChild(pDiv);
785
  });
786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
  pageWrapper.appendChild(div);
788
  con.appendChild(pageWrapper);
789
  });
@@ -817,18 +864,45 @@ INDEX_HTML = '''
817
 
818
  function loadNewComic() {
819
  fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
820
- const cleanData = data.map((p, pi) => ({
821
- panels: p.panels.map((pan, j) => ({
822
- src: `/frames/${pan.image}?sid=${sid}`,
823
- bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
824
- text: p.bubbles[j].dialog,
825
- left: (p.bubbles[j].bubble_offset_x || 50) + 'px',
826
- top: (p.bubbles[j].bubble_offset_y || 20) + 'px',
827
- type: (p.bubbles[j].type || 'speech'),
828
- classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`
829
- }] : []
830
- }))
831
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  renderFromState(cleanData); saveDraft(true);
833
  });
834
  }
@@ -914,9 +988,15 @@ INDEX_HTML = '''
914
  }
915
 
916
  function addBubble() {
917
- if(!selectedPanel) return alert("Select a panel first");
918
- const b = createBubbleHTML({ text: "Text", left: "50px", top: "30px", type: 'speech', classes: "speech-bubble speech tail-bottom" });
919
- selectedPanel.appendChild(b); selectBubble(b); saveDraft(true);
 
 
 
 
 
 
920
  }
921
 
922
  function deleteBubble() {
@@ -986,12 +1066,9 @@ INDEX_HTML = '''
986
  alert(`Exporting ${pgs.length} page(s)...`);
987
 
988
  // --- 0% ERROR FIX ---
989
- // 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
990
  const bubbles = document.querySelectorAll('.speech-bubble');
991
  bubbles.forEach(b => {
992
  const rect = b.getBoundingClientRect();
993
- // Add slight buffer (1px) to width to handle sub-pixel rendering differences
994
- // This prevents "just fitting" words from wrapping in the export
995
  b.style.width = (rect.width + 1) + 'px';
996
  b.style.height = rect.height + 'px';
997
  b.style.display = 'flex';
 
118
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
119
 
120
  if target_pages <= 0: target_pages = 1
121
+ panels_per_page = 5
 
122
  total_panels_needed = target_pages * panels_per_page
123
 
124
  selected_moments = []
 
176
  elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
177
  elif '?' in dialogue: b_type = 'speech'
178
 
179
+ # Use simpler positioning logic for the backend; user adjusts in frontend
180
+ # Just putting default offsets
181
+ bx, by = 50, 50
182
+
183
+ b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, type=b_type)
184
+ bubbles_list.append(b)
 
 
 
 
185
 
186
  pages = []
187
  for i in range(target_pages):
 
331
  /* ========================================= */
332
  /* 🎨 NEW POLYGON TEMPLATE LAYOUT CSS */
333
  /* ========================================= */
 
 
 
 
 
 
 
 
 
 
334
 
335
  .comic-wrapper { max-width: 1050px; margin: 0 auto; }
336
  .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
337
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
338
 
339
+ /*
340
+ The Comic Page Container
341
+ Width: 1000px, Height: 710px
342
+ Overflow must be VISIBLE so bubbles can pop out if needed,
343
+ but we usually want them contained.
344
+ Actually, to fix "missing bubbles", we must allow them to sit on top of everything.
345
+ */
346
  .comic-page {
347
  background: white;
348
  width: 1000px;
349
  height: 710px;
350
  box-shadow: 0 4px 10px rgba(0,0,0,0.1);
351
  position: relative;
352
+ /* Ensure bubbles (children of this) are not hidden by panel clipping */
353
+ z-index: 1;
354
  }
355
 
356
+ /*
357
+ PANELS:
358
+ Now defined with specific bounding boxes so images are not stretched/zoomed incorrectly.
359
+ Each panel has overflow:hidden to clip the image, but the bubbles will be siblings, not children.
360
+ */
361
  .panel {
362
  position: absolute;
363
  background: #eee;
364
  cursor: pointer;
365
+ overflow: hidden;
366
+ z-index: 0;
367
  }
368
+ .panel.selected { filter: brightness(0.9) sepia(0.2); z-index: 0; outline: 3px solid #2196F3; }
 
369
 
370
  .panel img {
371
  width: 100%;
 
377
  .panel img.pannable { cursor: grab; }
378
  .panel img.panning { cursor: grabbing; }
379
 
380
+ /* --- TIER 1 (Y: 0 - 350) --- */
381
+
382
+ /* Panel 0 (Top Left)
383
+ Starts at 0, ends at ~635.
384
+ Polygon: (0,0) -> (635.2, 0) -> (588.2, 350) -> (0, 350)
385
+ Box Width: ~636px. Box Height: 350px.
 
 
 
386
  */
 
 
 
387
  .panel-0 {
388
+ top: 0; left: 0; width: 636px; height: 350px;
389
+ clip-path: polygon(0% 0%, 100% 0%, 92.5% 100%, 0% 100%);
390
  }
391
 
392
+ /* Panel 1 (Top Right)
393
+ Starts at ~588 (to overlap/fit), ends at 1000.
394
+ Polygon: (635.2 + gap) ... actually lets follow the user coords:
395
+ Divider Top: 635.2, Bottom: 588.2.
396
+ So Right Panel starts after the divider.
397
+ Let's define Right Panel Box:
398
+ Left: 588px (min X), Width: 412px (1000-588).
399
+ Clip: (Top-Left of box is 588). Top-Left coord of polygon is 635.2. Relative X = 635.2 - 588 = 47.2.
400
+ So polygon starts at X=47.2px relative to box.
401
+ */
402
  .panel-1 {
403
+ top: 0; left: 588px; width: 412px; height: 350px;
404
+ clip-path: polygon(11.5% 0%, 100% 0%, 100% 100%, 0% 100%);
405
  }
406
 
407
+ /* --- TIER 2 (Y: 360 - 710) Height 350 --- */
408
+ /* Gutter is 10px, so T2 starts at 360px */
409
+
410
+ /* Panel 2 (Bottom Left)
411
+ Coords: (0,0) -> (293.2, 0) -> (326.2, 350) -> (0, 350)
412
+ Box: Left 0, Width 327px.
413
+ */
414
  .panel-2 {
415
+ top: 360px; left: 0; width: 327px; height: 350px;
416
+ clip-path: polygon(0% 0%, 89.6% 0%, 100% 100%, 0% 100%);
417
  }
418
 
419
+ /* Panel 3 (Bottom Middle)
420
+ Left Divider: Top 293.2, Bot 326.2.
421
+ Right Divider: Top 617.2, Bot 666.2.
422
+ Box Left: 293px. Width: 374px (667-293).
423
+ */
424
  .panel-3 {
425
+ top: 360px; left: 293px; width: 374px; height: 350px;
426
+ clip-path: polygon(0% 0%, 86.6% 0%, 100% 100%, 8.8% 100%);
427
  }
428
 
429
+ /* Panel 4 (Bottom Right)
430
+ Right Divider: Top 617.2, Bot 666.2.
431
+ Box Left: 617px. Width: 383px (1000-617).
432
+ */
433
  .panel-4 {
434
+ top: 360px; left: 617px; width: 383px; height: 350px;
435
+ clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 12.8% 100%);
436
  }
437
 
438
  /* SPEECH BUBBLES */
439
+ /* Positioned absolute relative to .comic-page now */
440
  .speech-bubble {
441
  position: absolute; display: flex; justify-content: center; align-items: center;
442
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
443
+ z-index: 100; /* Above panels */
444
+ cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
445
  font-size: 13px; text-align: center;
446
  overflow: visible;
447
  line-height: 1.2;
 
465
  border-radius: inherit;
466
  }
467
 
468
+ .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 101; }
469
  .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; }
470
 
471
  /* SPEECH BUBBLE CSS (Tails) */
 
734
  } catch(e) { console.error(e); alert("Failed to restore."); }
735
  }
736
 
737
+ // UPDATED STATE RETRIEVAL
738
+ // Bubbles are now children of comic-page, not panels.
739
+ // Panels need to be associated with bubbles still for data consistency.
740
+ // For simplicity, we will save bubbles as a list belonging to the page,
741
+ // but the backend format expects bubbles inside panels.
742
+ // We will attempt to reconstruct that structure roughly or just store them.
743
+ // Actually, let's just store "page bubbles" separately in our new save format if needed,
744
+ // OR we can map bubbles back to panels spatially.
745
+ // SIMPLER APPROACH: Save the structure as it is visually (Page -> Panels, Page -> Bubbles).
746
+
747
  function getCurrentState() {
748
  const pages = [];
749
  document.querySelectorAll('.comic-page').forEach(p => {
750
  const panels = [];
751
+ const bubbles = []; // Page-level bubbles
752
+
753
  p.querySelectorAll('.panel').forEach(pan => {
754
  const img = pan.querySelector('img');
 
 
 
 
 
 
 
 
 
 
 
 
755
  panels.push({
756
  src: img.src,
757
  zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
758
+ bubbles: [] // Legacy structure kept empty
759
+ });
760
+ });
761
+
762
+ p.querySelectorAll('.speech-bubble').forEach(b => {
763
+ const textEl = b.querySelector('.bubble-text');
764
+ bubbles.push({
765
+ text: textEl ? textEl.textContent : '',
766
+ left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height,
767
+ classes: b.className.replace(' selected', ''),
768
+ type: b.dataset.type, font: b.style.fontFamily,
769
+ tailPos: b.style.getPropertyValue('--tail-pos'),
770
+ colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
771
  });
772
  });
773
+
774
+ pages.push({ panels: panels, pageBubbles: bubbles });
775
  });
776
  return pages;
777
  }
 
789
  pageWrapper.appendChild(pageTitle);
790
 
791
  const div = document.createElement('div');
792
+ div.className = 'comic-page';
793
 
794
+ // 1. Render Panels
795
  page.panels.forEach((pan, idx) => {
796
  const pDiv = document.createElement('div');
 
797
  const posClass = `panel-${idx % 5}`;
798
  pDiv.className = `panel ${posClass}`;
799
 
 
804
  updateImageTransform(img);
805
  img.onmousedown = (e) => startPan(e, img);
806
  pDiv.appendChild(img);
807
+
808
+ // Legacy support: if bubbles were inside panels
809
+ (pan.bubbles || []).forEach(bData => {
810
+ // Convert old relative coords (if any) to page relative?
811
+ // Ideally data is standardized. We append to Page div, not pDiv.
812
+ const b = createBubbleHTML(bData);
813
+ div.appendChild(b);
814
+ });
815
+
816
  div.appendChild(pDiv);
817
  });
818
 
819
+ // 2. Render Page-Level Bubbles (New Structure)
820
+ if(page.pageBubbles) {
821
+ page.pageBubbles.forEach(bData => {
822
+ div.appendChild(createBubbleHTML(bData));
823
+ });
824
+ }
825
+
826
+ // Bind click to deselect
827
+ div.onclick = (e) => {
828
+ if(e.target === div) {
829
+ if(selectedBubble) selectedBubble.classList.remove('selected'); selectedBubble = null;
830
+ if(selectedPanel) selectedPanel.classList.remove('selected'); selectedPanel = null;
831
+ }
832
+ }
833
+
834
  pageWrapper.appendChild(div);
835
  con.appendChild(pageWrapper);
836
  });
 
864
 
865
  function loadNewComic() {
866
  fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
867
+ // Convert Backend Data (Bubbles in Panels) to Frontend Data (Page Bubbles)
868
+ const cleanData = data.map((p, pi) => {
869
+ const panels = [];
870
+ const pageBubbles = [];
871
+
872
+ // Panel Geometry for Default Bubble Placement mapping
873
+ // We have 5 panels. We know their rough centers.
874
+ const panelCenters = [
875
+ {x: 315, y: 175}, // 0
876
+ {x: 790, y: 175}, // 1
877
+ {x: 160, y: 535}, // 2
878
+ {x: 480, y: 535}, // 3
879
+ {x: 800, y: 535} // 4
880
+ ];
881
+
882
+ p.panels.forEach((pan, j) => {
883
+ panels.push({
884
+ src: `/frames/${pan.image}?sid=${sid}`,
885
+ bubbles: []
886
+ });
887
+
888
+ if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
889
+ // Backend gives an offset relative to panel 0,0 usually.
890
+ // But now panels have offsets.
891
+ // Let's just place bubble near the center of the panel.
892
+ const center = panelCenters[j % 5] || {x:500, y:350};
893
+
894
+ pageBubbles.push({
895
+ text: p.bubbles[j].dialog,
896
+ left: (center.x - 75) + 'px', // Center - half bubble width
897
+ top: (center.y - 40) + 'px',
898
+ type: (p.bubbles[j].type || 'speech'),
899
+ classes: `speech-bubble ${p.bubbles[j].type || 'speech'} tail-bottom`
900
+ });
901
+ }
902
+ });
903
+
904
+ return { panels: panels, pageBubbles: pageBubbles };
905
+ });
906
  renderFromState(cleanData); saveDraft(true);
907
  });
908
  }
 
988
  }
989
 
990
  function addBubble() {
991
+ // Add to the active page container
992
+ // If no panel is selected, finding the active page is tricky.
993
+ // We will default to the first page or the last clicked page context if we stored it.
994
+ // For now, if a panel is selected, add to that panel's parent page.
995
+ if(!selectedPanel) return alert("Select a panel to define which page to add to.");
996
+
997
+ const pageDiv = selectedPanel.parentElement;
998
+ const b = createBubbleHTML({ text: "Text", left: "50px", top: "50px", type: 'speech', classes: "speech-bubble speech tail-bottom" });
999
+ pageDiv.appendChild(b); selectBubble(b); saveDraft(true);
1000
  }
1001
 
1002
  function deleteBubble() {
 
1066
  alert(`Exporting ${pgs.length} page(s)...`);
1067
 
1068
  // --- 0% ERROR FIX ---
 
1069
  const bubbles = document.querySelectorAll('.speech-bubble');
1070
  bubbles.forEach(b => {
1071
  const rect = b.getBoundingClientRect();
 
 
1072
  b.style.width = (rect.width + 1) + 'px';
1073
  b.style.height = rect.height + 'px';
1074
  b.style.display = 'flex';