sdragly commited on
Commit
2db339f
·
1 Parent(s): 70ea19f

Area selection and background removal

Browse files
Files changed (2) hide show
  1. js/upload.js +160 -18
  2. style.css +158 -0
js/upload.js CHANGED
@@ -1,11 +1,16 @@
1
  import { CATEGORIES, getCategoryInfo, DEFAULT_ANCHORS } from './models.js';
2
  import { savePrincess, saveClothing } from './store.js';
 
 
3
  import { navigate } from './app.js';
4
 
5
- let uploadType = 'princess'; // 'princess' | 'clothes'
6
  let selectedCategory = 'dress';
7
- let imageBlob = null;
 
 
8
  let previewURL = null;
 
9
 
10
  export function initUpload() {
11
  const screen = document.getElementById('upload-screen');
@@ -28,6 +33,10 @@ export function initUpload() {
28
  </label>
29
  </div>
30
 
 
 
 
 
31
  <div id="category-section" class="category-picker" style="display:none">
32
  <p class="picker-label">What kind?</p>
33
  <div class="category-grid" id="category-grid"></div>
@@ -67,8 +76,10 @@ function bindEvents() {
67
  uploadType = btn.dataset.type;
68
  document.querySelectorAll('.type-toggle .pill').forEach(b => b.classList.remove('active'));
69
  btn.classList.add('active');
70
- document.getElementById('category-section').style.display = uploadType === 'clothes' ? '' : 'none';
71
- document.getElementById('asset-name').placeholder = uploadType === 'princess' ? 'Princess name...' : 'What is it?';
 
 
72
  updateSubmitState();
73
  });
74
  });
@@ -88,23 +99,52 @@ function bindEvents() {
88
  if (!file) return;
89
 
90
  imageBlob = file;
 
 
91
  if (previewURL) URL.revokeObjectURL(previewURL);
 
92
  previewURL = URL.createObjectURL(file);
 
93
 
94
- const label = document.getElementById('camera-label');
95
- label.innerHTML = `
96
- <img src="${previewURL}" class="camera-preview"/>
97
- <span class="camera-retake">Tap to retake</span>
98
- `;
99
- label.className = 'camera-preview-label';
100
  updateSubmitState();
101
  });
102
 
 
 
 
103
  // Name input
104
  document.getElementById('asset-name').addEventListener('input', updateSubmitState);
105
 
106
- // Submit
107
- document.getElementById('submit-btn').addEventListener('click', handleSubmit);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
  function updateSubmitState() {
@@ -113,18 +153,118 @@ function updateSubmitState() {
113
  btn.disabled = !imageBlob || !name;
114
  }
115
 
116
- async function handleSubmit() {
117
  const name = document.getElementById('asset-name').value.trim();
118
  if (!imageBlob || !name) return;
119
 
120
  const btn = document.getElementById('submit-btn');
121
  const status = document.getElementById('upload-status');
122
 
 
 
 
123
  btn.disabled = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  status.innerHTML = `
125
  <div class="processing">
126
  <span class="crown-spin">\u{1F451}</span>
127
- <p>Working some magic!</p>
128
  </div>
129
  `;
130
 
@@ -132,7 +272,7 @@ async function handleSubmit() {
132
  if (uploadType === 'princess') {
133
  await savePrincess({
134
  name,
135
- imageBlob,
136
  anchors: { ...DEFAULT_ANCHORS },
137
  });
138
  } else {
@@ -140,7 +280,7 @@ async function handleSubmit() {
140
  await saveClothing({
141
  name,
142
  category: selectedCategory,
143
- imageBlob,
144
  targetAnchor: catInfo.targetAnchor,
145
  scale: 0.4,
146
  zIndex: catInfo.zIndex,
@@ -153,7 +293,6 @@ async function handleSubmit() {
153
  <a href="#/" class="big-button">Back to game</a>
154
  </div>
155
  `;
156
- btn.style.display = 'none';
157
  } catch (err) {
158
  status.innerHTML = `
159
  <div class="error-state">
@@ -161,7 +300,6 @@ async function handleSubmit() {
161
  <p class="error-detail">${err.message}</p>
162
  </div>
163
  `;
164
- btn.disabled = false;
165
  }
166
  }
167
 
@@ -169,6 +307,10 @@ function resetState() {
169
  uploadType = 'princess';
170
  selectedCategory = 'dress';
171
  imageBlob = null;
 
 
172
  if (previewURL) URL.revokeObjectURL(previewURL);
 
173
  previewURL = null;
 
174
  }
 
1
  import { CATEGORIES, getCategoryInfo, DEFAULT_ANCHORS } from './models.js';
2
  import { savePrincess, saveClothing } from './store.js';
3
+ import { removeBackground, isModelLoaded } from './segmentation.js';
4
+ import { showPolygonSelector } from './polygon-select.js';
5
  import { navigate } from './app.js';
6
 
7
+ let uploadType = 'princess';
8
  let selectedCategory = 'dress';
9
+ let imageBlob = null; // original photo
10
+ let croppedBlob = null; // after polygon crop (or same as imageBlob if skipped)
11
+ let segmentedBlob = null; // after background removal
12
  let previewURL = null;
13
+ let croppedURL = null;
14
 
15
  export function initUpload() {
16
  const screen = document.getElementById('upload-screen');
 
33
  </label>
34
  </div>
35
 
36
+ <button id="select-area-btn" class="pill select-area-btn" style="display:none">
37
+ \u2702\uFE0F Select area
38
+ </button>
39
+
40
  <div id="category-section" class="category-picker" style="display:none">
41
  <p class="picker-label">What kind?</p>
42
  <div class="category-grid" id="category-grid"></div>
 
76
  uploadType = btn.dataset.type;
77
  document.querySelectorAll('.type-toggle .pill').forEach(b => b.classList.remove('active'));
78
  btn.classList.add('active');
79
+ document.getElementById('category-section').style.display =
80
+ uploadType === 'clothes' ? '' : 'none';
81
+ document.getElementById('asset-name').placeholder =
82
+ uploadType === 'princess' ? 'Princess name...' : 'What is it?';
83
  updateSubmitState();
84
  });
85
  });
 
99
  if (!file) return;
100
 
101
  imageBlob = file;
102
+ croppedBlob = null;
103
+ segmentedBlob = null;
104
  if (previewURL) URL.revokeObjectURL(previewURL);
105
+ if (croppedURL) URL.revokeObjectURL(croppedURL);
106
  previewURL = URL.createObjectURL(file);
107
+ croppedURL = null;
108
 
109
+ showPhotoPreview(previewURL);
110
+ document.getElementById('select-area-btn').style.display = '';
111
+ document.getElementById('upload-status').innerHTML = '';
112
+ document.getElementById('submit-btn').style.display = '';
 
 
113
  updateSubmitState();
114
  });
115
 
116
+ // Select area
117
+ document.getElementById('select-area-btn').addEventListener('click', handleSelectArea);
118
+
119
  // Name input
120
  document.getElementById('asset-name').addEventListener('input', updateSubmitState);
121
 
122
+ // Submit -> segmentation
123
+ document.getElementById('submit-btn').addEventListener('click', handleMagicTime);
124
+ }
125
+
126
+ function showPhotoPreview(url, caption) {
127
+ const label = document.getElementById('camera-label');
128
+ label.innerHTML = `
129
+ <img src="${url}" class="camera-preview"/>
130
+ <span class="camera-retake">${caption || 'Tap to retake'}</span>
131
+ `;
132
+ label.className = 'camera-preview-label';
133
+ }
134
+
135
+ async function handleSelectArea() {
136
+ if (!previewURL) return;
137
+
138
+ const result = await showPolygonSelector(previewURL);
139
+ if (!result) return; // cancelled
140
+
141
+ croppedBlob = result.imageBlob;
142
+ if (croppedURL) URL.revokeObjectURL(croppedURL);
143
+ croppedURL = URL.createObjectURL(croppedBlob);
144
+
145
+ showPhotoPreview(croppedURL, 'Area selected \u2014 tap to retake photo');
146
+ document.getElementById('select-area-btn').textContent = '\u2702\uFE0F Reselect area';
147
+ updateSubmitState();
148
  }
149
 
150
  function updateSubmitState() {
 
153
  btn.disabled = !imageBlob || !name;
154
  }
155
 
156
+ async function handleMagicTime() {
157
  const name = document.getElementById('asset-name').value.trim();
158
  if (!imageBlob || !name) return;
159
 
160
  const btn = document.getElementById('submit-btn');
161
  const status = document.getElementById('upload-status');
162
 
163
+ // Use cropped image if available, otherwise original
164
+ const inputBlob = croppedBlob || imageBlob;
165
+
166
  btn.disabled = true;
167
+ btn.style.display = 'none';
168
+
169
+ const modelHint = isModelLoaded()
170
+ ? ''
171
+ : '<p class="progress-hint">First time takes a moment to download the AI model.</p>';
172
+
173
+ status.innerHTML = `
174
+ <div class="processing">
175
+ <span class="crown-spin">\u{1F451}</span>
176
+ <p id="seg-message">Removing the background!</p>
177
+ <div id="seg-progress-bar" class="progress-bar"><div id="seg-progress-fill" class="progress-fill"></div></div>
178
+ ${modelHint}
179
+ </div>
180
+ `;
181
+
182
+ try {
183
+ segmentedBlob = await removeBackground(inputBlob, (info) => {
184
+ const msg = document.getElementById('seg-message');
185
+ const fill = document.getElementById('seg-progress-fill');
186
+ if (msg) msg.textContent = info.message;
187
+ if (fill && info.progress != null) {
188
+ fill.style.width = `${info.progress}%`;
189
+ }
190
+ });
191
+
192
+ // Show preview with approve/retry
193
+ const segURL = URL.createObjectURL(segmentedBlob);
194
+ const beforeURL = croppedURL || previewURL;
195
+ status.innerHTML = `
196
+ <div class="seg-preview">
197
+ <p class="seg-preview-label">How does it look?</p>
198
+ <div class="seg-comparison">
199
+ <div class="seg-compare-item">
200
+ <img src="${beforeURL}" class="seg-compare-img" alt="Before"/>
201
+ <span>Before</span>
202
+ </div>
203
+ <div class="seg-compare-item">
204
+ <img src="${segURL}" class="seg-compare-img checkerboard-bg" alt="After"/>
205
+ <span>After</span>
206
+ </div>
207
+ </div>
208
+ <div class="seg-actions">
209
+ <button id="seg-approve" class="big-button">\u2705 Looks good!</button>
210
+ <button id="seg-skip" class="pill">Save without removing</button>
211
+ <button id="seg-retry" class="pill">Retake photo</button>
212
+ </div>
213
+ </div>
214
+ `;
215
+
216
+ document.getElementById('seg-approve').addEventListener('click', () => saveAsset(name, segmentedBlob));
217
+ document.getElementById('seg-skip').addEventListener('click', () => saveAsset(name, inputBlob));
218
+ document.getElementById('seg-retry').addEventListener('click', resetToCamera);
219
+
220
+ } catch (err) {
221
+ console.error('Segmentation failed:', err);
222
+ status.innerHTML = `
223
+ <div class="error-state">
224
+ <p>Background removal didn't work this time.</p>
225
+ <p class="error-detail">${err.message}</p>
226
+ <div class="seg-actions">
227
+ <button id="seg-skip-err" class="big-button">Save without removing</button>
228
+ <button id="seg-retry-err" class="pill">Try again</button>
229
+ </div>
230
+ </div>
231
+ `;
232
+ document.getElementById('seg-skip-err').addEventListener('click', () => saveAsset(name, inputBlob));
233
+ document.getElementById('seg-retry-err').addEventListener('click', () => {
234
+ btn.style.display = '';
235
+ btn.disabled = false;
236
+ status.innerHTML = '';
237
+ });
238
+ }
239
+ }
240
+
241
+ function resetToCamera() {
242
+ const status = document.getElementById('upload-status');
243
+ const btn = document.getElementById('submit-btn');
244
+ status.innerHTML = '';
245
+ btn.style.display = '';
246
+ btn.disabled = false;
247
+ document.getElementById('camera-input').value = '';
248
+ const label = document.getElementById('camera-label');
249
+ label.innerHTML = `
250
+ <span class="camera-icon">\u{1F4F8}</span>
251
+ <span class="camera-text">Tap to photograph!</span>
252
+ `;
253
+ label.className = 'camera-label';
254
+ document.getElementById('select-area-btn').style.display = 'none';
255
+ document.getElementById('select-area-btn').textContent = '\u2702\uFE0F Select area';
256
+ imageBlob = null;
257
+ croppedBlob = null;
258
+ segmentedBlob = null;
259
+ updateSubmitState();
260
+ }
261
+
262
+ async function saveAsset(name, blob) {
263
+ const status = document.getElementById('upload-status');
264
  status.innerHTML = `
265
  <div class="processing">
266
  <span class="crown-spin">\u{1F451}</span>
267
+ <p>Saving!</p>
268
  </div>
269
  `;
270
 
 
272
  if (uploadType === 'princess') {
273
  await savePrincess({
274
  name,
275
+ imageBlob: blob,
276
  anchors: { ...DEFAULT_ANCHORS },
277
  });
278
  } else {
 
280
  await saveClothing({
281
  name,
282
  category: selectedCategory,
283
+ imageBlob: blob,
284
  targetAnchor: catInfo.targetAnchor,
285
  scale: 0.4,
286
  zIndex: catInfo.zIndex,
 
293
  <a href="#/" class="big-button">Back to game</a>
294
  </div>
295
  `;
 
296
  } catch (err) {
297
  status.innerHTML = `
298
  <div class="error-state">
 
300
  <p class="error-detail">${err.message}</p>
301
  </div>
302
  `;
 
303
  }
304
  }
305
 
 
307
  uploadType = 'princess';
308
  selectedCategory = 'dress';
309
  imageBlob = null;
310
+ croppedBlob = null;
311
+ segmentedBlob = null;
312
  if (previewURL) URL.revokeObjectURL(previewURL);
313
+ if (croppedURL) URL.revokeObjectURL(croppedURL);
314
  previewURL = null;
315
+ croppedURL = null;
316
  }
style.css CHANGED
@@ -361,6 +361,7 @@ h2 { font-size: 1.5rem; font-weight: 700; color: var(--lavender-dk); }
361
  }
362
  .name-input:focus { border-color: var(--gold); }
363
 
 
364
  .magic-btn { align-self: center; font-size: 1.4rem; margin-top: 0.5rem; }
365
 
366
  .processing {
@@ -492,3 +493,160 @@ h2 { font-size: 1.5rem; font-weight: 700; color: var(--lavender-dk); }
492
  .tray-item {
493
  touch-action: none;
494
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  }
362
  .name-input:focus { border-color: var(--gold); }
363
 
364
+ .select-area-btn { align-self: center; }
365
  .magic-btn { align-self: center; font-size: 1.4rem; margin-top: 0.5rem; }
366
 
367
  .processing {
 
493
  .tray-item {
494
  touch-action: none;
495
  }
496
+
497
+ /* ---- Segmentation progress ---- */
498
+
499
+ .progress-bar {
500
+ width: 80%;
501
+ max-width: 250px;
502
+ height: 12px;
503
+ border-radius: 6px;
504
+ background: var(--lavender);
505
+ overflow: hidden;
506
+ margin-top: 0.5rem;
507
+ }
508
+
509
+ .progress-fill {
510
+ height: 100%;
511
+ width: 0%;
512
+ background: var(--gold);
513
+ border-radius: 6px;
514
+ transition: width 0.3s ease;
515
+ }
516
+
517
+ .progress-hint {
518
+ font-size: 0.85rem;
519
+ color: var(--lavender-dk);
520
+ text-align: center;
521
+ margin-top: 0.5rem;
522
+ }
523
+
524
+ /* ---- Segmentation preview ---- */
525
+
526
+ .seg-preview {
527
+ display: flex;
528
+ flex-direction: column;
529
+ align-items: center;
530
+ gap: 1rem;
531
+ padding: 0.5rem;
532
+ }
533
+
534
+ .seg-preview-label {
535
+ font-size: 1.3rem;
536
+ font-weight: 700;
537
+ }
538
+
539
+ .seg-comparison {
540
+ display: flex;
541
+ gap: 0.75rem;
542
+ justify-content: center;
543
+ width: 100%;
544
+ }
545
+
546
+ .seg-compare-item {
547
+ display: flex;
548
+ flex-direction: column;
549
+ align-items: center;
550
+ gap: 0.3rem;
551
+ flex: 1;
552
+ max-width: 180px;
553
+ font-size: 0.9rem;
554
+ font-weight: 700;
555
+ color: var(--lavender-dk);
556
+ }
557
+
558
+ .seg-compare-img {
559
+ width: 100%;
560
+ aspect-ratio: 3/4;
561
+ object-fit: contain;
562
+ border-radius: 1rem;
563
+ border: 3px solid var(--lavender);
564
+ background: var(--panel);
565
+ }
566
+
567
+ .checkerboard-bg {
568
+ background-image:
569
+ linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
570
+ linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
571
+ linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
572
+ linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
573
+ background-size: 16px 16px;
574
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
575
+ }
576
+
577
+ .seg-actions {
578
+ display: flex;
579
+ flex-direction: column;
580
+ align-items: center;
581
+ gap: 0.6rem;
582
+ width: 100%;
583
+ }
584
+
585
+ /* ---- Polygon selection overlay ---- */
586
+
587
+ .poly-overlay {
588
+ position: fixed;
589
+ inset: 0;
590
+ z-index: 1000;
591
+ background: var(--parchment);
592
+ display: flex;
593
+ flex-direction: column;
594
+ overflow: hidden;
595
+ }
596
+
597
+ .poly-toolbar {
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: space-between;
601
+ padding: 0.5rem 0.75rem;
602
+ gap: 0.5rem;
603
+ flex-shrink: 0;
604
+ }
605
+
606
+ .poly-hint {
607
+ font-size: 1rem;
608
+ font-weight: 700;
609
+ color: var(--lavender-dk);
610
+ text-align: center;
611
+ flex: 1;
612
+ }
613
+
614
+ .poly-undo-btn, .poly-close-btn {
615
+ font-size: 0.9rem;
616
+ padding: 0.4rem 0.8rem;
617
+ }
618
+
619
+ .poly-canvas-wrap {
620
+ flex: 1;
621
+ min-height: 0;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ position: relative;
626
+ padding: 0.5rem;
627
+ overflow: hidden;
628
+ }
629
+
630
+ .poly-img {
631
+ max-width: 100%;
632
+ max-height: 100%;
633
+ object-fit: contain;
634
+ border-radius: 0.5rem;
635
+ display: block;
636
+ }
637
+
638
+ .poly-canvas {
639
+ position: absolute;
640
+ touch-action: none;
641
+ cursor: crosshair;
642
+ }
643
+
644
+ .poly-actions {
645
+ display: flex;
646
+ flex-direction: column;
647
+ align-items: center;
648
+ gap: 0.5rem;
649
+ padding: 0.75rem;
650
+ padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
651
+ flex-shrink: 0;
652
+ }