mlmihjaz Claude Opus 4.8 commited on
Commit
dede77e
·
1 Parent(s): 34f3659

Add advanced vector tracing + persistent Library

Browse files

- Image -> Vector tracing via ImageTracer.js: new Vector tab with B&W,
1-color silhouette, and 2/3/4/5-color quantized modes + detail slider.
Source from selected image, clipboard paste, or upload. Result is an
editable vector group (ungroup to edit each color).
- My Library tab: save any object/vector (with thumbnail) to localStorage,
click to re-add, delete, and Export/Import the whole library as JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (2) hide show
  1. WORKLOG.md +5 -0
  2. index.html +188 -1
WORKLOG.md CHANGED
@@ -53,6 +53,11 @@ curl -sL https://mlmihjaz-ltm.static.hf.space/ | grep -c <some-unique-string-you
53
  - **Save / Load projects:** `.dfp.json` export (**💾 Save** / Ctrl+S) + **📂 Open** import; **autosave** to `localStorage df_autosave` (debounced 800ms via `scheduleAutosave` in `onCanvasChange`), **restored on load** (`restoreAutosave`); **✲ New** clears. Project shape: `{app,v,name,w,h,json}`.
54
  - Verified: JS `node --check` clean, all new IDs/functions resolve, served page contains them. Browser-tested visuals = snap lines + arrange menu.
55
 
 
 
 
 
 
56
  ## Known notes / next ideas
57
  - Templates are placeholders — biggest remaining "real Canva" feature.
58
  - Counter relies on a 3rd-party free service; if it dies, swap for a tiny Gradio/Docker Space hosting a JSON counter.
 
53
  - **Save / Load projects:** `.dfp.json` export (**💾 Save** / Ctrl+S) + **📂 Open** import; **autosave** to `localStorage df_autosave` (debounced 800ms via `scheduleAutosave` in `onCanvasChange`), **restored on load** (`restoreAutosave`); **✲ New** clears. Project shape: `{app,v,name,w,h,json}`.
54
  - Verified: JS `node --check` clean, all new IDs/functions resolve, served page contains them. Browser-tested visuals = snap lines + arrange menu.
55
 
56
+ ## Features implemented (2026-06-16 session #2 — vector tracing + library)
57
+ - **Vector tracing (raster→SVG):** uses **ImageTracer.js** (CDN: `cdn.jsdelivr.net/gh/jankovicsandras/imagetracerjs@master/imagetracer_v1.2.6.js`, global `ImageTracer`). New **Vector** left tab (`sec-trace`). Source = selected canvas image / paste / upload (`traceSourceEl`). Color modes: **B&W**, **1 color** (silhouette), **2–5 color** (`numberofcolors`). `runTrace(mode)`: downscale to ≤900px → `imagedataToSVG` → `fabric.loadSVGFromString` → `groupSVGElements` → add as `_type:'vector'` group (ungroup to edit each color). B&W/1-color pre-threshold the imagedata + use a black/white `pal`; 1-color also drops near-white paths. Detail slider maps to `ltres`/`pathomit`. Wrapped in `setTimeout` so the UI can paint a "Tracing…" status. Cross-origin/tainted images are caught + toasted.
58
+ - **My Library (persistent):** new **Library** tab (`sec-library`), `localStorage df_library`. `addToLibrary()` serializes the active object (auto-groups an activeSelection) via `toObject(['_name','_type'])` + a PNG thumbnail (`obj.toDataURL`). `renderLibrary()` grid (click=add via `fabric.util.enlivenObjects`, hover ×=delete). **Export/Import** the whole library as JSON ("for future versions"). Quota errors are caught.
59
+ - Verified: `node --check` clean, all new IDs/functions resolve, ImageTracer CDN 200. Browser-test the trace quality / tune detault detail.
60
+
61
  ## Known notes / next ideas
62
  - Templates are placeholders — biggest remaining "real Canva" feature.
63
  - Counter relies on a 3rd-party free service; if it dies, swap for a tiny Gradio/Docker Space hosting a JSON counter.
index.html CHANGED
@@ -6,6 +6,7 @@
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>DesignForge Pro</title>
8
  <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
 
9
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
10
  <style>
11
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
@@ -130,6 +131,11 @@ body{font-family:var(--sans);background:var(--bg);color:var(--txt);height:100vh;
130
  .thumb-item{aspect-ratio:4/3;border-radius:var(--r);overflow:hidden;cursor:pointer;position:relative;background:var(--surf3);border:1.5px solid transparent;transition:all .12s}
131
  .thumb-item:hover{border-color:var(--acc);transform:scale(1.02);box-shadow:var(--sh)}
132
  .thumb-item img{width:100%;height:100%;object-fit:cover;display:block}
 
 
 
 
 
133
  .thumb-item .thumb-label{position:absolute;bottom:0;left:0;right:0;padding:4px 6px;background:linear-gradient(transparent,rgba(0,0,0,.55));color:#fff;font-size:9px;font-weight:600}
134
 
135
  /* SHAPES — category grid */
@@ -402,6 +408,14 @@ input[type="file"]{display:none}
402
  <svg viewBox="0 0 20 20" fill="none"><path d="M3 14l4-4 4 4 4-6 3 6H3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="6" cy="7" r="2" stroke="currentColor" stroke-width="1.5"/></svg>
403
  <span class="tab-label">BG</span>
404
  </div>
 
 
 
 
 
 
 
 
405
  <div style="flex:1"></div>
406
  <div class="tab-icon" data-panel="layers">
407
  <svg viewBox="0 0 20 20" fill="none"><path d="M3 7l7-4 7 4-7 4-7-4zM3 13l7 4 7-4M3 10l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
@@ -630,6 +644,44 @@ input[type="file"]{display:none}
630
  </div>
631
 
632
  <!-- LAYERS -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  <div class="panel-section" id="sec-layers">
634
  <div id="layers-list"></div>
635
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-top:8px">
@@ -2205,11 +2257,129 @@ function newDesign() {
2205
  toast('New design', 'ok');
2206
  }
2207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2208
  // ============================================================
2209
  // PANEL SWITCHING
2210
  // ============================================================
2211
  let activePanelId = 'upload';
2212
- const PANEL_TITLES = { upload:'Upload', templates:'Templates', photos:'Photos', elements:'Shapes', text:'Text', background:'Background', layers:'Layers' };
2213
  // Always open the given panel (used by auto-switch on selection)
2214
  function openPanel(id) {
2215
  qsa('.tab-icon').forEach(t => t.classList.remove('active'));
@@ -2562,6 +2732,23 @@ document.addEventListener('DOMContentLoaded', () => {
2562
  $i('btn-open-proj').onclick = () => $i('proj-file-input').click();
2563
  $i('proj-file-input').addEventListener('change', e => { if (e.target.files[0]) openProjectFile(e.target.files[0]); e.target.value = ''; });
2564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2565
  // VISITOR COUNTER (counterapi.dev — free, no auth; display = base + live count)
2566
  (function visitorCounter() {
2567
  const BASE = 0, NS = 'designforge-ltm-mlmihjaz', KEY = 'visits'; // server counter seeded at 1990
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>DesignForge Pro</title>
8
  <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/gh/jankovicsandras/imagetracerjs@master/imagetracer_v1.2.6.js"></script>
10
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
11
  <style>
12
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
 
131
  .thumb-item{aspect-ratio:4/3;border-radius:var(--r);overflow:hidden;cursor:pointer;position:relative;background:var(--surf3);border:1.5px solid transparent;transition:all .12s}
132
  .thumb-item:hover{border-color:var(--acc);transform:scale(1.02);box-shadow:var(--sh)}
133
  .thumb-item img{width:100%;height:100%;object-fit:cover;display:block}
134
+ .p-btn.tr-mode.sel{background:var(--acc);color:#fff;border-color:transparent}
135
+ .lib-item img{object-fit:contain;background:repeating-conic-gradient(#e9e9ef 0% 25%,#fff 0% 50%) 50%/14px 14px}
136
+ .lib-del{position:absolute;top:3px;right:3px;width:18px;height:18px;border:none;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;font-size:12px;line-height:1;cursor:pointer;display:none;align-items:center;justify-content:center;padding:0}
137
+ .thumb-item:hover .lib-del{display:flex}
138
+ .lib-name{position:absolute;left:0;right:0;bottom:0;font-size:9px;padding:2px 4px;background:rgba(0,0,0,.5);color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
139
  .thumb-item .thumb-label{position:absolute;bottom:0;left:0;right:0;padding:4px 6px;background:linear-gradient(transparent,rgba(0,0,0,.55));color:#fff;font-size:9px;font-weight:600}
140
 
141
  /* SHAPES — category grid */
 
408
  <svg viewBox="0 0 20 20" fill="none"><path d="M3 14l4-4 4 4 4-6 3 6H3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="6" cy="7" r="2" stroke="currentColor" stroke-width="1.5"/></svg>
409
  <span class="tab-label">BG</span>
410
  </div>
411
+ <div class="tab-icon" data-panel="trace">
412
+ <svg viewBox="0 0 20 20" fill="none"><path d="M3 16c4-1 4-11 8-11 3 0 2 6 5 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="3" cy="16" r="1.6" fill="currentColor"/><circle cx="16" cy="11" r="1.6" fill="currentColor"/></svg>
413
+ <span class="tab-label">Vector</span>
414
+ </div>
415
+ <div class="tab-icon" data-panel="library">
416
+ <svg viewBox="0 0 20 20" fill="none"><path d="M4 3v14M7 3v14M11 4l4 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="2.5" y="3" width="3" height="14" rx="1" stroke="currentColor" stroke-width="1.3"/></svg>
417
+ <span class="tab-label">Library</span>
418
+ </div>
419
  <div style="flex:1"></div>
420
  <div class="tab-icon" data-panel="layers">
421
  <svg viewBox="0 0 20 20" fill="none"><path d="M3 7l7-4 7 4-7 4-7-4zM3 13l7 4 7-4M3 10l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
 
644
  </div>
645
 
646
  <!-- LAYERS -->
647
+ <!-- VECTORIZE / TRACE -->
648
+ <div class="panel-section" id="sec-trace">
649
+ <div class="sec-label">Image → Vector <span class="badge green">Trace</span></div>
650
+ <p style="font-size:11px;color:var(--txt3);margin:0 0 10px">Convert a photo or logo into clean, scalable vector shapes. Pick how many colors to keep.</p>
651
+ <div class="sec-label">1 · Choose source</div>
652
+ <button class="p-btn full" id="tr-use-sel" style="margin-bottom:5px">🎯 Use selected image</button>
653
+ <button class="p-btn full" id="tr-paste" style="margin-bottom:5px">⎘ Paste image from clipboard</button>
654
+ <button class="p-btn full" id="tr-upload">⬆ Upload an image</button>
655
+ <input type="file" id="tr-file" accept="image/*" hidden>
656
+ <div class="sec-label" style="margin-top:12px">2 · Color mode</div>
657
+ <div id="tr-modes" style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
658
+ <button class="p-btn tr-mode" data-mode="bw">B&amp;W</button>
659
+ <button class="p-btn tr-mode" data-mode="1">1 color</button>
660
+ <button class="p-btn tr-mode" data-mode="2">2 color</button>
661
+ <button class="p-btn tr-mode" data-mode="3">3 color</button>
662
+ <button class="p-btn tr-mode" data-mode="4">4 color</button>
663
+ <button class="p-btn tr-mode" data-mode="5">5 color</button>
664
+ </div>
665
+ <div class="sec-label" style="margin-top:12px">Detail</div>
666
+ <input type="range" id="tr-detail" min="0" max="3" step="1" value="1" style="width:100%">
667
+ <div style="display:flex;justify-content:space-between;font-size:10px;color:var(--txt3)"><span>Smooth</span><span>Detailed</span></div>
668
+ <div id="tr-status" style="font-size:11px;color:var(--acc);margin-top:10px;display:none"></div>
669
+ <p style="font-size:10.5px;color:var(--txt3);margin-top:12px">Tip: after tracing, ungroup (Ctrl+Shift+G) to edit each color shape, or save it to your Library →</p>
670
+ </div>
671
+
672
+ <!-- LIBRARY -->
673
+ <div class="panel-section" id="sec-library">
674
+ <div class="sec-label">My Library <span class="badge green">Saved</span></div>
675
+ <button class="p-btn acc full" id="lib-save" style="margin-bottom:8px">+ Save selection to Library</button>
676
+ <div style="display:flex;gap:6px;margin-bottom:10px">
677
+ <button class="p-btn" id="lib-export" style="flex:1">⬇ Export</button>
678
+ <button class="p-btn" id="lib-import-btn" style="flex:1">⬆ Import</button>
679
+ <input type="file" id="lib-file" accept=".json,application/json" hidden>
680
+ </div>
681
+ <div id="lib-empty" style="font-size:11px;color:var(--txt3);margin-bottom:8px">Nothing saved yet. Select any object (or a traced vector) and tap “Save to Library”.</div>
682
+ <div class="thumb-grid" id="lib-grid"></div>
683
+ </div>
684
+
685
  <div class="panel-section" id="sec-layers">
686
  <div id="layers-list"></div>
687
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-top:8px">
 
2257
  toast('New design', 'ok');
2258
  }
2259
 
2260
+ // ============================================================
2261
+ // VECTOR TRACING (raster -> SVG via ImageTracer.js) + MY LIBRARY
2262
+ // ============================================================
2263
+ let traceSourceEl = null; // HTMLImageElement queued for tracing
2264
+
2265
+ function getSelectedImageEl() {
2266
+ const o = canvas.getActiveObject();
2267
+ return (o && o.type === 'image') ? (o._element || o.getElement()) : null;
2268
+ }
2269
+ function setTraceSourceFromFile(file) {
2270
+ const r = new FileReader();
2271
+ r.onload = () => { const im = new Image(); im.onload = () => { traceSourceEl = im; toast('Source ready — pick a color mode', 'ok'); }; im.src = r.result; };
2272
+ r.readAsDataURL(file);
2273
+ }
2274
+ function imageToImageData(imgEl, maxDim) {
2275
+ let w = imgEl.naturalWidth || imgEl.width, h = imgEl.naturalHeight || imgEl.height;
2276
+ const s = Math.min(1, maxDim / Math.max(w, h));
2277
+ w = Math.max(1, Math.round(w * s)); h = Math.max(1, Math.round(h * s));
2278
+ const c = document.createElement('canvas'); c.width = w; c.height = h;
2279
+ const ctx = c.getContext('2d'); ctx.drawImage(imgEl, 0, 0, w, h);
2280
+ return ctx.getImageData(0, 0, w, h);
2281
+ }
2282
+ function thresholdImageData(imgd) {
2283
+ const d = imgd.data;
2284
+ for (let i = 0; i < d.length; i += 4) {
2285
+ const lum = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
2286
+ const v = lum < 128 ? 0 : 255; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255;
2287
+ }
2288
+ return imgd;
2289
+ }
2290
+ function traceOptionsFor(mode, detail) {
2291
+ const ltres = [3, 1.5, 1, 0.5][detail] != null ? [3, 1.5, 1, 0.5][detail] : 1;
2292
+ const pathomit = [16, 8, 4, 1][detail] != null ? [16, 8, 4, 1][detail] : 8;
2293
+ const base = { ltres, qtres: ltres, pathomit, strokewidth: 0, linefilter: true, scale: 1, roundcoords: 1, viewbox: false, desc: false, colorsampling: 2, colorquantcycles: 3, blurradius: 0 };
2294
+ const blackWhitePal = [{ r: 0, g: 0, b: 0, a: 255 }, { r: 255, g: 255, b: 255, a: 255 }];
2295
+ if (mode === 'bw') return Object.assign(base, { numberofcolors: 2, pal: blackWhitePal, _bw: true });
2296
+ if (mode === '1') return Object.assign(base, { numberofcolors: 2, pal: blackWhitePal, _one: true });
2297
+ return Object.assign(base, { numberofcolors: parseInt(mode) });
2298
+ }
2299
+ function runTrace(mode) {
2300
+ if (typeof ImageTracer === 'undefined') { toast('Tracer still loading — try again in a second', 'warn'); return; }
2301
+ const el = traceSourceEl || getSelectedImageEl();
2302
+ if (!el) { toast('Choose a source image first', 'warn'); return; }
2303
+ const detail = parseInt($i('tr-detail').value) || 1;
2304
+ const opts = traceOptionsFor(mode, detail);
2305
+ const st = $i('tr-status'); st.style.display = 'block'; st.textContent = '⏳ Tracing… this can take a moment';
2306
+ setStatus('Tracing image…');
2307
+ setTimeout(() => {
2308
+ try {
2309
+ let imgd = imageToImageData(el, 900);
2310
+ if (opts._bw || opts._one) imgd = thresholdImageData(imgd);
2311
+ const svgstr = ImageTracer.imagedataToSVG(imgd, opts);
2312
+ fabric.loadSVGFromString(svgstr, (objects, options) => {
2313
+ let objs = (objects || []).filter(Boolean);
2314
+ if (opts._one) objs = objs.filter(o => { const f = new fabric.Color(o.fill || '#000').getSource(); return (f[0] + f[1] + f[2]) < 384; });
2315
+ if (!objs.length) { st.style.display = 'none'; setStatus('Ready'); toast('Nothing traceable in this image', 'warn'); return; }
2316
+ const g = fabric.util.groupSVGElements(objs, options);
2317
+ const sc = Math.min(canvas.width * 0.6 / (g.width || 1), canvas.height * 0.6 / (g.height || 1), 4);
2318
+ g.set({ left: canvas.width / 2, top: canvas.height / 2, originX: 'center', originY: 'center', scaleX: sc, scaleY: sc });
2319
+ g._name = 'Vector (' + (mode === 'bw' ? 'B&W' : mode + ' color') + ')'; g._type = 'vector';
2320
+ canvas.add(g); canvas.setActiveObject(g); canvas.requestRenderAll();
2321
+ updateLayers(); saveHist();
2322
+ st.style.display = 'none'; setStatus('Ready'); toast('Vectorized! 🎉', 'ok');
2323
+ });
2324
+ } catch (e) {
2325
+ console.error(e); st.style.display = 'none'; setStatus('Ready');
2326
+ toast('Trace failed — the image may be cross-origin protected. Try Upload/Paste.', 'err');
2327
+ }
2328
+ }, 40);
2329
+ }
2330
+
2331
+ // ---- My Library (persistent, localStorage) -------------------------------
2332
+ function loadLib() { try { return JSON.parse(localStorage.getItem('df_library') || '[]'); } catch (e) { return []; } }
2333
+ function saveLib(arr) { try { localStorage.setItem('df_library', JSON.stringify(arr)); return true; } catch (e) { toast('Library storage full — export & remove some items', 'warn'); return false; } }
2334
+ function addToLibrary() {
2335
+ let o = canvas.getActiveObject();
2336
+ if (!o) { toast('Select an object to save first', 'warn'); return; }
2337
+ if (o.type === 'activeSelection') { o.toGroup(); canvas.requestRenderAll(); o = canvas.getActiveObject(); }
2338
+ const json = o.toObject(['_name', '_type']);
2339
+ let thumb = '';
2340
+ try { thumb = o.toDataURL({ format: 'png', multiplier: Math.min(1, 90 / Math.max(o.getScaledWidth(), o.getScaledHeight())) }); } catch (e) {}
2341
+ const arr = loadLib();
2342
+ arr.unshift({ id: 'L' + Date.now(), name: o._name || o.type || 'Item', json, thumb, ts: Date.now() });
2343
+ if (saveLib(arr)) { renderLibrary(); toast('Saved to Library', 'ok'); }
2344
+ }
2345
+ function addLibraryItemToCanvas(item) {
2346
+ fabric.util.enlivenObjects([item.json], objs => {
2347
+ const o = objs[0]; if (!o) return;
2348
+ o.set({ left: canvas.width / 2, top: canvas.height / 2, originX: 'center', originY: 'center' });
2349
+ o._name = item.name; canvas.add(o); canvas.setActiveObject(o); canvas.requestRenderAll();
2350
+ updateLayers(); saveHist(); toast('Added from Library', 'ok');
2351
+ });
2352
+ }
2353
+ function renderLibrary() {
2354
+ const arr = loadLib(), grid = $i('lib-grid'), empty = $i('lib-empty');
2355
+ if (!grid) return;
2356
+ grid.innerHTML = ''; empty.style.display = arr.length ? 'none' : 'block';
2357
+ arr.forEach(item => {
2358
+ const d = document.createElement('div'); d.className = 'thumb-item lib-item'; d.title = item.name;
2359
+ const safe = (item.name || '').replace(/[<>&"]/g, '');
2360
+ d.innerHTML = `<img src="${item.thumb || ''}" alt=""><span class="lib-name">${safe}</span><button class="lib-del" title="Delete">×</button>`;
2361
+ d.querySelector('img').onclick = () => addLibraryItemToCanvas(item);
2362
+ d.querySelector('.lib-del').onclick = e => { e.stopPropagation(); saveLib(loadLib().filter(x => x.id !== item.id)); renderLibrary(); };
2363
+ grid.appendChild(d);
2364
+ });
2365
+ }
2366
+ function exportLibrary() {
2367
+ const blob = new Blob([JSON.stringify(loadLib())], { type: 'application/json' });
2368
+ const u = URL.createObjectURL(blob); const a = document.createElement('a');
2369
+ a.href = u; a.download = 'designforge-library.json'; a.click(); URL.revokeObjectURL(u);
2370
+ toast('Library exported', 'ok');
2371
+ }
2372
+ function importLibrary(file) {
2373
+ const r = new FileReader();
2374
+ r.onload = () => { try { const inc = JSON.parse(r.result); if (!Array.isArray(inc)) throw 0; const cur = loadLib(); const ids = new Set(cur.map(x => x.id)); inc.forEach(x => { if (!ids.has(x.id)) cur.push(x); }); saveLib(cur); renderLibrary(); toast('Library imported', 'ok'); } catch (e) { toast('Invalid library file', 'err'); } };
2375
+ r.readAsText(file);
2376
+ }
2377
+
2378
  // ============================================================
2379
  // PANEL SWITCHING
2380
  // ============================================================
2381
  let activePanelId = 'upload';
2382
+ const PANEL_TITLES = { upload:'Upload', templates:'Templates', photos:'Photos', elements:'Shapes', text:'Text', background:'Background', trace:'Vectorize', library:'My Library', layers:'Layers' };
2383
  // Always open the given panel (used by auto-switch on selection)
2384
  function openPanel(id) {
2385
  qsa('.tab-icon').forEach(t => t.classList.remove('active'));
 
2732
  $i('btn-open-proj').onclick = () => $i('proj-file-input').click();
2733
  $i('proj-file-input').addEventListener('change', e => { if (e.target.files[0]) openProjectFile(e.target.files[0]); e.target.value = ''; });
2734
 
2735
+ // VECTOR TRACE
2736
+ $i('tr-use-sel').onclick = () => { const el = getSelectedImageEl(); if (!el) { toast('Select an image on the canvas first', 'warn'); return; } traceSourceEl = el; toast('Source set — pick a color mode', 'ok'); };
2737
+ $i('tr-upload').onclick = () => $i('tr-file').click();
2738
+ $i('tr-file').addEventListener('change', e => { if (e.target.files[0]) setTraceSourceFromFile(e.target.files[0]); e.target.value = ''; });
2739
+ $i('tr-paste').onclick = async () => {
2740
+ try { const items = await navigator.clipboard.read(); for (const it of items) for (const t of it.types) if (t.startsWith('image/')) { setTraceSourceFromFile(await it.getType(t)); return; } toast('No image in clipboard', 'err'); }
2741
+ catch { toast('Clipboard blocked — use Upload instead', 'err'); }
2742
+ };
2743
+ qsa('#tr-modes .tr-mode').forEach(b => b.onclick = () => { qsa('#tr-modes .tr-mode').forEach(x => x.classList.remove('sel')); b.classList.add('sel'); runTrace(b.dataset.mode); });
2744
+
2745
+ // LIBRARY
2746
+ $i('lib-save').onclick = addToLibrary;
2747
+ $i('lib-export').onclick = exportLibrary;
2748
+ $i('lib-import-btn').onclick = () => $i('lib-file').click();
2749
+ $i('lib-file').addEventListener('change', e => { if (e.target.files[0]) importLibrary(e.target.files[0]); e.target.value = ''; });
2750
+ renderLibrary();
2751
+
2752
  // VISITOR COUNTER (counterapi.dev — free, no auth; display = base + live count)
2753
  (function visitorCounter() {
2754
  const BASE = 0, NS = 'designforge-ltm-mlmihjaz', KEY = 'visits'; // server counter seeded at 1990