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>
- WORKLOG.md +5 -0
- 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&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
|