LogicalTrue commited on
Commit
3a6bde5
·
1 Parent(s): bd8d298

Add interactive Cropper.js modal for cropping and framing subjects before 3D model generation

Browse files
Files changed (3) hide show
  1. frontend/app.js +98 -0
  2. frontend/index.html +38 -0
  3. frontend/viewer.html +129 -0
frontend/app.js CHANGED
@@ -356,6 +356,104 @@ removePreviewBtn.addEventListener('click', (e) => {
356
  generate3dBtn.disabled = true;
357
  });
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  // 2D Image Generation
360
  generate2dBtn.addEventListener('click', async () => {
361
  const prompt = promptInput.value.trim();
 
356
  generate3dBtn.disabled = true;
357
  });
358
 
359
+ // Cropper.js Integration for PC Mode
360
+ let pcCropper = null;
361
+
362
+ const btnOpenCropPC = document.getElementById('btnOpenCropPC');
363
+ const cropModalOverlay = document.getElementById('cropModalOverlay');
364
+ const cropImageTarget = document.getElementById('cropImageTarget');
365
+ const btnCancelCrop = document.getElementById('btnCancelCrop');
366
+ const btnApplyCrop = document.getElementById('btnApplyCrop');
367
+ const btnCropAspectFree = document.getElementById('btnCropAspectFree');
368
+ const btnCropAspectSquare = document.getElementById('btnCropAspectSquare');
369
+ const btnCropReset = document.getElementById('btnCropReset');
370
+
371
+ function initCropperPC(imageUrl) {
372
+ if (!cropModalOverlay || !cropImageTarget) return;
373
+
374
+ cropImageTarget.src = imageUrl;
375
+ cropModalOverlay.style.display = 'flex';
376
+
377
+ if (pcCropper) {
378
+ pcCropper.destroy();
379
+ pcCropper = null;
380
+ }
381
+
382
+ setTimeout(() => {
383
+ pcCropper = new Cropper(cropImageTarget, {
384
+ aspectRatio: 1,
385
+ viewMode: 1,
386
+ autoCropArea: 0.85,
387
+ responsive: true,
388
+ background: false
389
+ });
390
+ }, 150);
391
+ }
392
+
393
+ if (btnOpenCropPC) {
394
+ btnOpenCropPC.addEventListener('click', () => {
395
+ if (active2dImageURL) {
396
+ initCropperPC(active2dImageURL);
397
+ }
398
+ });
399
+ }
400
+
401
+ if (btnCancelCrop) {
402
+ btnCancelCrop.addEventListener('click', () => {
403
+ if (cropModalOverlay) cropModalOverlay.style.display = 'none';
404
+ if (pcCropper) {
405
+ pcCropper.destroy();
406
+ pcCropper = null;
407
+ }
408
+ });
409
+ }
410
+
411
+ if (btnCropAspectFree) {
412
+ btnCropAspectFree.addEventListener('click', () => {
413
+ if (pcCropper) pcCropper.setAspectRatio(NaN);
414
+ });
415
+ }
416
+
417
+ if (btnCropAspectSquare) {
418
+ btnCropAspectSquare.addEventListener('click', () => {
419
+ if (pcCropper) pcCropper.setAspectRatio(1);
420
+ });
421
+ }
422
+
423
+ if (btnCropReset) {
424
+ btnCropReset.addEventListener('click', () => {
425
+ if (pcCropper) pcCropper.reset();
426
+ });
427
+ }
428
+
429
+ if (btnApplyCrop) {
430
+ btnApplyCrop.addEventListener('click', () => {
431
+ if (!pcCropper) return;
432
+
433
+ const croppedCanvas = pcCropper.getCroppedCanvas({
434
+ width: 1024,
435
+ height: 1024,
436
+ imageSmoothingEnabled: true,
437
+ imageSmoothingQuality: 'high'
438
+ });
439
+
440
+ if (croppedCanvas) {
441
+ croppedCanvas.toBlob((blob) => {
442
+ if (blob) {
443
+ const croppedFile = new File([blob], 'cropped_image.png', { type: 'image/png' });
444
+ handleImageFile(croppedFile);
445
+ showCustomToast('Imagen recortada con éxito.');
446
+ }
447
+ if (cropModalOverlay) cropModalOverlay.style.display = 'none';
448
+ if (pcCropper) {
449
+ pcCropper.destroy();
450
+ pcCropper = null;
451
+ }
452
+ }, 'image/png');
453
+ }
454
+ });
455
+ }
456
+
457
  // 2D Image Generation
458
  generate2dBtn.addEventListener('click', async () => {
459
  const prompt = promptInput.value.trim();
frontend/index.html CHANGED
@@ -29,6 +29,10 @@
29
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
30
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/exporters/GLTFExporter.js"></script>
31
 
 
 
 
 
32
  <!-- Custom Style -->
33
  <link rel="stylesheet" href="styles.css">
34
  </head>
@@ -115,6 +119,10 @@
115
  <div class="preview-container" id="previewContainer" style="margin-top: 1rem;">
116
  <img src="" alt="Preview" class="preview-img" id="imagePreview">
117
  <button class="remove-preview" id="removePreviewBtn" title="Quitar imagen">&times;</button>
 
 
 
 
118
  </div>
119
  </div>
120
 
@@ -713,6 +721,36 @@
713
  </div>
714
  </div>
715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  <script type="module" src="app.js?v=118"></script>
717
  <script>
718
  // Initialize Lucide icons on page load
 
29
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
30
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/exporters/GLTFExporter.js"></script>
31
 
32
+ <!-- Cropper.js for Interactive Image Cropping -->
33
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
34
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
35
+
36
  <!-- Custom Style -->
37
  <link rel="stylesheet" href="styles.css">
38
  </head>
 
119
  <div class="preview-container" id="previewContainer" style="margin-top: 1rem;">
120
  <img src="" alt="Preview" class="preview-img" id="imagePreview">
121
  <button class="remove-preview" id="removePreviewBtn" title="Quitar imagen">&times;</button>
122
+ <button type="button" id="btnOpenCropPC" style="margin-top: 8px; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 0.82rem; padding: 8px 12px; background: rgba(124, 58, 237, 0.15); border: 1px solid rgba(124, 58, 237, 0.4); color: #a78bfa; border-radius: 8px; cursor: pointer; font-weight: 600;">
123
+ <i data-lucide="crop" style="width: 14px; height: 14px;"></i>
124
+ <span>Enmarcar / Recortar Sujeto</span>
125
+ </button>
126
  </div>
127
  </div>
128
 
 
721
  </div>
722
  </div>
723
 
724
+ <!-- Interactive Crop Modal Overlay -->
725
+ <div id="cropModalOverlay" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: space-between; padding: 16px; box-sizing: border-box;">
726
+ <!-- Modal Header -->
727
+ <div style="width: 100%; max-width: 650px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1);">
728
+ <div style="font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 1.1rem; color: #fff; display: flex; align-items: center; gap: 8px;">
729
+ <i data-lucide="crop" style="width: 18px; height: 18px; color: #a78bfa;"></i>
730
+ <span>Enmarcar / Recortar Sujeto</span>
731
+ </div>
732
+ <button id="btnCancelCrop" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1rem; display: flex; align-items: center; justify-content: center;">✕</button>
733
+ </div>
734
+
735
+ <!-- Crop Canvas Viewport Container -->
736
+ <div style="width: 100%; max-width: 650px; height: calc(100vh - 180px); max-height: 520px; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; margin: 12px 0; background: #000; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);">
737
+ <img id="cropImageTarget" src="" style="max-width: 100%; max-height: 100%;">
738
+ </div>
739
+
740
+ <!-- Modal Controls Footer -->
741
+ <div style="width: 100%; max-width: 650px; display: flex; flex-direction: column; gap: 10px;">
742
+ <div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;">
743
+ <button type="button" id="btnCropAspectFree" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Libre</button>
744
+ <button type="button" id="btnCropAspectSquare" style="background: rgba(124,58,237,0.3); border: 1px solid #7c3aed; color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">1:1 Cuadrado (Recomendado)</button>
745
+ <button type="button" id="btnCropReset" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Reiniciar</button>
746
+ </div>
747
+ <button type="button" id="btnApplyCrop" style="width: 100%; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; padding: 12px; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; box-shadow: 0 4px 12px rgba(16,185,129,0.3); display: flex; align-items: center; justify-content: center; gap: 6px;">
748
+ <i data-lucide="check" style="width: 18px; height: 18px;"></i>
749
+ <span>Confirmar Recorte</span>
750
+ </button>
751
+ </div>
752
+ </div>
753
+
754
  <script type="module" src="app.js?v=118"></script>
755
  <script>
756
  // Initialize Lucide icons on page load
frontend/viewer.html CHANGED
@@ -6,6 +6,10 @@
6
  <title>23DFactory - Visualizador Móvil</title>
7
  <!-- Import Google's model-viewer component -->
8
  <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
 
 
 
 
9
  <style>
10
  body, html {
11
  margin: 0;
@@ -361,6 +365,9 @@
361
  <div id="imagePreviewContainer" style="display: none; position: relative;">
362
  <img id="imagePreviewMobile" style="max-width: 100%; max-height: 250px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); margin: 0 auto; display: block;">
363
  <button id="btnRemoveImageMobile" style="position: absolute; top: 8px; right: 8px; background: rgba(239,68,68,0.9); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 0.8rem;">✕</button>
 
 
 
364
  </div>
365
  </div>
366
 
@@ -455,6 +462,34 @@
455
  </div>
456
  </div>
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  <script>
459
  const urlParams = new URLSearchParams(window.location.search);
460
  const modelUrl = urlParams.get('model');
@@ -1075,6 +1110,100 @@
1075
  });
1076
  }
1077
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
  // 2D AI Generation
1079
  if (btnGen2DMobile) {
1080
  btnGen2DMobile.addEventListener('click', async () => {
 
6
  <title>23DFactory - Visualizador Móvil</title>
7
  <!-- Import Google's model-viewer component -->
8
  <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
9
+
10
+ <!-- Cropper.js for Interactive Image Cropping -->
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
13
  <style>
14
  body, html {
15
  margin: 0;
 
365
  <div id="imagePreviewContainer" style="display: none; position: relative;">
366
  <img id="imagePreviewMobile" style="max-width: 100%; max-height: 250px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); margin: 0 auto; display: block;">
367
  <button id="btnRemoveImageMobile" style="position: absolute; top: 8px; right: 8px; background: rgba(239,68,68,0.9); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 0.8rem;">✕</button>
368
+ <button type="button" id="btnOpenCropMobile" style="margin-top: 10px; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 0.85rem; padding: 10px; background: rgba(124, 58, 237, 0.2); border: 1px solid rgba(124, 58, 237, 0.5); color: #a78bfa; border-radius: 8px; cursor: pointer; font-weight: 600;">
369
+ ✂️ Enmarcar / Recortar Sujeto
370
+ </button>
371
  </div>
372
  </div>
373
 
 
462
  </div>
463
  </div>
464
 
465
+ <!-- Interactive Crop Modal Overlay -->
466
+ <div id="cropModalOverlay" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: space-between; padding: 16px; box-sizing: border-box;">
467
+ <!-- Modal Header -->
468
+ <div style="width: 100%; max-width: 600px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1);">
469
+ <div style="font-weight: 700; font-size: 1rem; color: #fff; display: flex; align-items: center; gap: 8px;">
470
+ ✂️ Enmarcar / Recortar Sujeto
471
+ </div>
472
+ <button id="btnCancelCrop" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1rem; display: flex; align-items: center; justify-content: center;">✕</button>
473
+ </div>
474
+
475
+ <!-- Crop Canvas Viewport Container -->
476
+ <div style="width: 100%; max-width: 600px; height: calc(100vh - 180px); max-height: 480px; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; margin: 12px 0; background: #000; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);">
477
+ <img id="cropImageTarget" src="" style="max-width: 100%; max-height: 100%;">
478
+ </div>
479
+
480
+ <!-- Modal Controls Footer -->
481
+ <div style="width: 100%; max-width: 600px; display: flex; flex-direction: column; gap: 10px;">
482
+ <div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;">
483
+ <button type="button" id="btnCropAspectFree" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Libre</button>
484
+ <button type="button" id="btnCropAspectSquare" style="background: rgba(124,58,237,0.3); border: 1px solid #7c3aed; color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">1:1 Cuadrado (Recomendado)</button>
485
+ <button type="button" id="btnCropReset" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Reiniciar</button>
486
+ </div>
487
+ <button type="button" id="btnApplyCrop" style="width: 100%; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; padding: 12px; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; box-shadow: 0 4px 12px rgba(16,185,129,0.3); display: flex; align-items: center; justify-content: center; gap: 6px;">
488
+ ✔ Confirmar Recorte
489
+ </button>
490
+ </div>
491
+ </div>
492
+
493
  <script>
494
  const urlParams = new URLSearchParams(window.location.search);
495
  const modelUrl = urlParams.get('model');
 
1110
  });
1111
  }
1112
 
1113
+ // Cropper.js Integration for Mobile View
1114
+ let mobileCropper = null;
1115
+
1116
+ const btnOpenCropMobile = document.getElementById('btnOpenCropMobile');
1117
+ const cropModalOverlay = document.getElementById('cropModalOverlay');
1118
+ const cropImageTarget = document.getElementById('cropImageTarget');
1119
+ const btnCancelCrop = document.getElementById('btnCancelCrop');
1120
+ const btnApplyCrop = document.getElementById('btnApplyCrop');
1121
+ const btnCropAspectFree = document.getElementById('btnCropAspectFree');
1122
+ const btnCropAspectSquare = document.getElementById('btnCropAspectSquare');
1123
+ const btnCropReset = document.getElementById('btnCropReset');
1124
+
1125
+ function initCropperMobile(imageUrl) {
1126
+ if (!cropModalOverlay || !cropImageTarget) return;
1127
+
1128
+ cropImageTarget.src = imageUrl;
1129
+ cropModalOverlay.style.display = 'flex';
1130
+
1131
+ if (mobileCropper) {
1132
+ mobileCropper.destroy();
1133
+ mobileCropper = null;
1134
+ }
1135
+
1136
+ setTimeout(() => {
1137
+ mobileCropper = new Cropper(cropImageTarget, {
1138
+ aspectRatio: 1,
1139
+ viewMode: 1,
1140
+ autoCropArea: 0.85,
1141
+ responsive: true,
1142
+ background: false
1143
+ });
1144
+ }, 150);
1145
+ }
1146
+
1147
+ if (btnOpenCropMobile) {
1148
+ btnOpenCropMobile.addEventListener('click', () => {
1149
+ if (base64Image) {
1150
+ initCropperMobile(base64Image);
1151
+ }
1152
+ });
1153
+ }
1154
+
1155
+ if (btnCancelCrop) {
1156
+ btnCancelCrop.addEventListener('click', () => {
1157
+ if (cropModalOverlay) cropModalOverlay.style.display = 'none';
1158
+ if (mobileCropper) {
1159
+ mobileCropper.destroy();
1160
+ mobileCropper = null;
1161
+ }
1162
+ });
1163
+ }
1164
+
1165
+ if (btnCropAspectFree) {
1166
+ btnCropAspectFree.addEventListener('click', () => {
1167
+ if (mobileCropper) mobileCropper.setAspectRatio(NaN);
1168
+ });
1169
+ }
1170
+
1171
+ if (btnCropAspectSquare) {
1172
+ btnCropAspectSquare.addEventListener('click', () => {
1173
+ if (mobileCropper) mobileCropper.setAspectRatio(1);
1174
+ });
1175
+ }
1176
+
1177
+ if (btnCropReset) {
1178
+ btnCropReset.addEventListener('click', () => {
1179
+ if (mobileCropper) mobileCropper.reset();
1180
+ });
1181
+ }
1182
+
1183
+ if (btnApplyCrop) {
1184
+ btnApplyCrop.addEventListener('click', () => {
1185
+ if (!mobileCropper) return;
1186
+
1187
+ const croppedCanvas = mobileCropper.getCroppedCanvas({
1188
+ width: 1024,
1189
+ height: 1024,
1190
+ imageSmoothingEnabled: true,
1191
+ imageSmoothingQuality: 'high'
1192
+ });
1193
+
1194
+ if (croppedCanvas) {
1195
+ base64Image = croppedCanvas.toDataURL('image/png');
1196
+ if (imagePreviewMobile) imagePreviewMobile.src = base64Image;
1197
+
1198
+ if (cropModalOverlay) cropModalOverlay.style.display = 'none';
1199
+ if (mobileCropper) {
1200
+ mobileCropper.destroy();
1201
+ mobileCropper = null;
1202
+ }
1203
+ }
1204
+ });
1205
+ }
1206
+
1207
  // 2D AI Generation
1208
  if (btnGen2DMobile) {
1209
  btnGen2DMobile.addEventListener('click', async () => {