5m4ck3r commited on
Commit
6ea923a
·
verified ·
1 Parent(s): 833b8d0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1018 -19
index.html CHANGED
@@ -1,19 +1,1018 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>LogicSpine PolyPath v2.0</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/@phosphor-icons/web"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@jaames/iro@5"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
11
+ <style>
12
+ body { margin: 0; overflow: hidden; background-color: #0a0a0a; color: #fff; touch-action: none; }
13
+
14
+ /* The Viewport holds the camera */
15
+ #viewport {
16
+ position: relative; width: 100%; height: 100%; overflow: hidden;
17
+ background: radial-gradient(circle, #1a1a1a 0%, #000000 100%);
18
+ /* Checkerboard for transparency indication */
19
+ background-image: linear-gradient(45deg, #111 25%, transparent 25%),
20
+ linear-gradient(-45deg, #111 25%, transparent 25%),
21
+ linear-gradient(45deg, transparent 75%, #111 75%),
22
+ linear-gradient(-45deg, transparent 75%, #111 75%);
23
+ background-size: 20px 20px;
24
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
25
+ cursor: grab;
26
+ }
27
+ #viewport:active { cursor: grabbing; }
28
+ #viewport.drawing-mode { cursor: crosshair; }
29
+
30
+ /* The Camera Container that actually moves and scales */
31
+ #camera {
32
+ position: absolute; top: 0; left: 0;
33
+ transform-origin: 0 0;
34
+ box-shadow: 0 0 100px rgba(0,0,0,0.8);
35
+ }
36
+ canvas { display: block; image-rendering: pixelated; } /* Keeps pixels sharp when zoomed */
37
+
38
+ #overlay-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
39
+
40
+ /* DOM Objects (Text/Images) */
41
+ .draggable-obj {
42
+ position: absolute; pointer-events: auto; cursor: move;
43
+ border: 2px dashed transparent; box-sizing: border-box;
44
+ transform-origin: center center;
45
+ }
46
+ .draggable-obj:hover, .draggable-obj.active { border-color: #6366f1; }
47
+
48
+ .editable-text {
49
+ outline: none; min-width: 50px; font-family: sans-serif; white-space: nowrap;
50
+ }
51
+
52
+ /* Handles */
53
+ .handle {
54
+ position: absolute; width: 14px; height: 14px; background: #fff;
55
+ border: 2px solid #6366f1; border-radius: 50%; display: none;
56
+ }
57
+ .draggable-obj.active .handle { display: block; }
58
+ .resize-handle { bottom: -7px; right: -7px; cursor: nwse-resize; }
59
+ .rotate-handle { top: -25px; left: calc(50% - 7px); cursor: crosshair; }
60
+ /* Line connecting rotate handle to box */
61
+ .draggable-obj.active::before {
62
+ content: ''; position: absolute; top: -18px; left: calc(50% - 1px);
63
+ width: 2px; height: 18px; background: #6366f1;
64
+ }
65
+
66
+ /* Color Picker Popover */
67
+ #colorPickerUI {
68
+ position: absolute; top: 20%; left: 100px; z-index: 100;
69
+ display: none; box-shadow: 0 20px 50px rgba(0,0,0,0.5);
70
+ }
71
+
72
+ /* Gradient Text Support */
73
+ .gradient-text {
74
+ background-clip: text; -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent; color: transparent;
76
+ }
77
+
78
+ /* Loading Screen */
79
+ #globalLoader {
80
+ backdrop-filter: blur(10px);
81
+ transition: opacity 0.3s ease;
82
+ }
83
+
84
+ ::-webkit-scrollbar { width: 8px; }
85
+ ::-webkit-scrollbar-track { background: #111; }
86
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
87
+ </style>
88
+ </head>
89
+ <body class="flex flex-col h-screen font-sans selection:bg-indigo-500 selection:text-white" oncontextmenu="return false;">
90
+
91
+ <header class="h-16 bg-neutral-900 border-b border-neutral-800 flex items-center justify-between px-6 z-30 shrink-0">
92
+ <div class="flex items-center gap-3">
93
+ <h1 class="text-xl font-bold tracking-widest text-white transition-all"><a href="https://logicspine.in" target="_blank" class="hover:opacity-80 transition-opacity cursor-pointer">LOGIC<span class="text-indigo-500">SPINE</span></a><span class="ml-2 px-2 py-0.5 bg-indigo-500/10 border border-indigo-500/20 rounded text-indigo-400 text-sm align-middle font-mono">PolyPath</span></h1>
94
+ <span class="px-2 py-1 text-xs bg-neutral-800 text-neutral-400 rounded-md font-mono"> v2.0</span>
95
+ </div>
96
+
97
+ <div class="flex items-center gap-4">
98
+ <button onclick="document.getElementById('fileUpload').click()" class="flex items-center gap-2 hover:text-indigo-400 text-sm font-medium transition-colors">
99
+ <i class="ph ph-upload-simple text-lg"></i> Open Image
100
+ </button>
101
+ <input type="file" id="fileUpload" class="hidden" accept="image/*" onchange="loadImage(event)">
102
+
103
+ <div class="h-6 w-px bg-neutral-700"></div>
104
+
105
+ <button onclick="undo()" class="hover:text-white text-neutral-400 transition-colors" title="Undo"><i class="ph ph-arrow-u-up-left text-xl"></i></button>
106
+ <button onclick="redo()" class="hover:text-white text-neutral-400 transition-colors" title="Redo"><i class="ph ph-arrow-u-up-right text-xl"></i></button>
107
+
108
+ <div class="h-6 w-px bg-neutral-700"></div>
109
+
110
+ <span class="text-xs text-neutral-500 font-mono" id="zoomLevelIndicator">100%</span>
111
+ <button onclick="resetCamera()" class="hover:text-white text-neutral-400 text-xs" title="Reset View"><i class="ph ph-corners-out"></i></button>
112
+
113
+ <div class="h-6 w-px bg-neutral-700 mx-2"></div>
114
+
115
+ <button onclick="openModal('vectorModal')" class="flex items-center gap-2 px-4 py-2 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded-lg text-sm font-medium transition-all">
116
+ <i class="ph ph-bezier-curve"></i> Vector Art
117
+ </button>
118
+ <button onclick="openModal('exportModal')" class="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-sm font-medium shadow-[0_0_15px_rgba(79,70,229,0.3)] transition-all">
119
+ <i class="ph ph-download-simple"></i> Export UHD
120
+ </button>
121
+
122
+ <button onclick="openModal('pdfModal')" class="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg text-sm font-medium shadow-[0_0_15px_rgba(220,38,38,0.3)] transition-all">
123
+ <i class="ph ph-file-pdf"></i> Export PDF
124
+ </button>
125
+ </div>
126
+ </header>
127
+
128
+ <div class="flex flex-1 overflow-hidden relative">
129
+
130
+ <aside class="w-20 bg-neutral-900 border-r border-neutral-800 flex flex-col items-center py-6 gap-6 z-30 shrink-0">
131
+ <button class="tool-btn text-indigo-400" data-tool="select" title="Select & Move Objects" onclick="setTool('select')">
132
+ <i class="ph ph-cursor text-2xl mb-1"></i><span class="text-[10px]">Select</span>
133
+ </button>
134
+ <button class="tool-btn text-neutral-400 hover:text-white" data-tool="erase" title="Freehand Eraser" onclick="setTool('erase')">
135
+ <i class="ph ph-eraser text-2xl mb-1"></i><span class="text-[10px]">Erase</span>
136
+ </button>
137
+ <button class="tool-btn text-neutral-400 hover:text-white" data-tool="replaceColor" title="Replace Specific Color" onclick="setTool('replaceColor')">
138
+ <i class="ph ph-palette text-2xl mb-1"></i><span class="text-[10px]">Replace</span>
139
+ </button>
140
+ <button class="tool-btn text-neutral-400 hover:text-white" data-tool="removeColor" title="Remove Color to Transparent" onclick="setTool('removeColor')">
141
+ <i class="ph ph-drop text-2xl mb-1"></i><span class="text-[10px]">Remove</span>
142
+ </button>
143
+ <button class="tool-btn text-neutral-400 hover:text-white" data-tool="canny" title="Area Fill Eraser" onclick="setTool('canny')">
144
+ <i class="ph ph-paint-bucket text-2xl mb-1"></i><span class="text-[10px]">Area Fill</span>
145
+ </button>
146
+ <div class="w-10 h-px bg-neutral-800"></div>
147
+ <button class="tool-btn text-neutral-400 hover:text-white" onclick="addTextObject()">
148
+ <i class="ph ph-text-t text-2xl mb-1"></i><span class="text-[10px]">Text</span>
149
+ </button>
150
+ <button class="tool-btn text-neutral-400 hover:text-white" onclick="document.getElementById('addOverlayImage').click()">
151
+ <i class="ph ph-image text-2xl mb-1"></i><span class="text-[10px]">Image</span>
152
+ </button>
153
+ <input type="file" id="addOverlayImage" class="hidden" accept="image/*" onchange="addOverlayImage(event)">
154
+ </aside>
155
+
156
+ <main id="viewport" class="flex-1">
157
+ <div id="camera">
158
+ <canvas id="mainCanvas"></canvas>
159
+ <canvas id="edgeOverlay" style="position:absolute; top:0; left:0; pointer-events:none; opacity:0.8; image-rendering:pixelated;"></canvas>
160
+ <div id="overlay-layer"></div>
161
+ </div>
162
+ </main>
163
+
164
+ <aside id="propertiesPanel" class="w-64 bg-neutral-900 border-l border-neutral-800 p-4 hidden flex-col gap-4 z-30 shrink-0">
165
+ <h3 class="text-xs font-bold text-neutral-500 uppercase tracking-wider mb-2">Properties</h3>
166
+ <div id="dynamicProperties" class="flex flex-col gap-4"></div>
167
+ </aside>
168
+ </div>
169
+
170
+ <div id="colorPickerUI" class="bg-neutral-900 border border-neutral-700 p-4 rounded-xl w-72">
171
+ <div class="flex justify-between items-center mb-4">
172
+ <span class="text-sm font-bold text-white">Color Settings</span>
173
+ <button onclick="closeColorPicker()" class="text-neutral-500 hover:text-white"><i class="ph ph-x"></i></button>
174
+ </div>
175
+
176
+ <div id="pickerContainer" class="flex justify-center mb-4"></div>
177
+
178
+ <div class="grid grid-cols-2 gap-2 mb-4">
179
+ <div>
180
+ <label class="text-xs text-neutral-500">HEX</label>
181
+ <input type="text" id="hexInput" class="w-full bg-neutral-800 border border-neutral-700 rounded p-1 text-sm text-center font-mono text-white outline-none focus:border-indigo-500">
182
+ </div>
183
+ <div>
184
+ <label class="text-xs text-neutral-500">RGB</label>
185
+ <input type="text" id="rgbInput" class="w-full bg-neutral-800 border border-neutral-700 rounded p-1 text-sm text-center font-mono text-white outline-none focus:border-indigo-500">
186
+ </div>
187
+ </div>
188
+
189
+ <button id="eyedropperBtn" onclick="activateEyedropper()" class="w-full py-2 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded flex items-center justify-center gap-2 text-sm transition-colors">
190
+ <i class="ph ph-eyedropper text-indigo-400"></i> Pick from Canvas
191
+ </button>
192
+ </div>
193
+
194
+ <div id="globalLoader" class="fixed inset-0 bg-black/60 z-50 flex-col items-center justify-center hidden pointer-events-none">
195
+ <i class="ph ph-spinner-gap animate-spin text-5xl text-indigo-500 mb-4"></i>
196
+ <h2 class="text-xl font-bold tracking-widest text-white" id="loaderText">PROCESSING...</h2>
197
+ <p class="text-sm text-neutral-400 mt-2">Computing neural pathways and vector math.</p>
198
+ </div>
199
+
200
+ <div id="exportModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden items-center justify-center z-50 px-4">
201
+ <div class="bg-neutral-900 border border-neutral-800 rounded-xl p-6 w-full max-w-md shadow-2xl">
202
+ <div class="flex justify-between items-center mb-6">
203
+ <h2 class="text-xl font-bold">Export UHD PNG</h2>
204
+ <button onclick="closeModal('exportModal')" class="text-neutral-500 hover:text-white"><i class="ph ph-x text-xl"></i></button>
205
+ </div>
206
+
207
+ <label class="block text-sm text-neutral-400 mb-2">Resolution Preset (Width)</label>
208
+ <select id="exportPreset" class="w-full bg-neutral-800 border border-neutral-700 rounded-lg p-3 text-white mb-4 outline-none focus:border-indigo-500" onchange="document.getElementById('customResDiv').style.display = this.value === 'custom' ? 'block' : 'none'">
209
+ <option value="original">Original Size</option>
210
+ <option value="2000">High (2000px)</option>
211
+ <option value="4000">Ultra (4000px)</option>
212
+ <option value="6000">UHD Print (6000px)</option>
213
+ <option value="custom">Custom Width...</option>
214
+ </select>
215
+
216
+ <div id="customResDiv" class="hidden mb-4">
217
+ <label class="block text-sm text-neutral-400 mb-2">Custom Max Width (px)</label>
218
+ <input type="number" id="customWidth" value="8000" class="w-full bg-neutral-800 border border-neutral-700 rounded-lg p-3 text-white outline-none focus:border-indigo-500">
219
+ </div>
220
+
221
+ <p class="text-xs text-neutral-500 mb-6">Note: This will bake all text and layers into a single high-resolution PNG image.</p>
222
+
223
+ <button onclick="executeExport()" class="w-full py-3 bg-indigo-600 hover:bg-indigo-500 rounded-lg font-bold transition-colors">Download Now</button>
224
+ </div>
225
+ </div>
226
+
227
+ <div id="vectorModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden items-center justify-center z-50 px-4">
228
+ <div class="bg-neutral-900 border border-neutral-800 rounded-xl p-6 w-full max-w-md shadow-2xl">
229
+ <div class="flex justify-between items-center mb-6">
230
+ <h2 class="text-xl font-bold">Create Vector Art</h2>
231
+ <button onclick="closeModal('vectorModal')" class="text-neutral-500 hover:text-white"><i class="ph ph-x text-xl"></i></button>
232
+ </div>
233
+
234
+ <label class="block text-sm text-neutral-400 mb-2">Vector Quality Level</label>
235
+ <select id="vectorQuality" class="w-full bg-neutral-800 border border-neutral-700 rounded-lg p-3 text-white mb-6 outline-none focus:border-indigo-500">
236
+ <option value="low">Low (Fastest, Flat Colors)</option>
237
+ <option value="mid">Mid (Good for Logos)</option>
238
+ <option value="high">High (Detailed Artwork)</option>
239
+ <option value="super" selected>Super High (Max Detail, Heavy File)</option>
240
+ </select>
241
+
242
+ <button onclick="executeVectorize()" class="w-full py-3 bg-indigo-600 hover:bg-indigo-500 rounded-lg font-bold transition-colors">Generate SVG</button>
243
+ </div>
244
+ </div>
245
+
246
+ <div id="pdfModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden items-center justify-center z-50 px-4">
247
+ <div class="bg-neutral-900 border border-neutral-800 rounded-xl p-6 w-full max-w-md shadow-2xl">
248
+ <div class="flex justify-between items-center mb-6">
249
+ <h2 class="text-xl font-bold">Export Project Report (PDF)</h2>
250
+ <button onclick="closeModal('pdfModal')" class="text-neutral-500 hover:text-white"><i class="ph ph-x text-xl"></i></button>
251
+ </div>
252
+
253
+ <label class="block text-sm text-neutral-400 mb-2">Vector Quality</label>
254
+ <select id="pdfQuality" class="w-full bg-neutral-800 border border-neutral-700 rounded-lg p-3 text-white mb-4 outline-none focus:border-indigo-500">
255
+ <option value="low">Low (Fast Generation)</option>
256
+ <option value="super" selected>Super High (Max Detail)</option>
257
+ </select>
258
+
259
+ <label class="block text-sm text-neutral-400 mb-2">UHD Render Width (px)</label>
260
+ <input type="number" id="pdfWidth" value="6000" class="w-full bg-neutral-800 border border-neutral-700 rounded-lg p-3 text-white mb-6 outline-none focus:border-indigo-500">
261
+
262
+ <p class="text-xs text-neutral-500 mb-6">Note: This generates a massive 3-page PDF containing the SVG render, UHD render, and Original workspace.</p>
263
+
264
+ <button onclick="executePDFExport()" class="w-full py-3 bg-red-600 hover:bg-red-500 rounded-lg font-bold transition-colors">Generate PDF</button>
265
+ </div>
266
+ </div>
267
+
268
+ <script>
269
+ // --- Core Elements ---
270
+ const viewport = document.getElementById('viewport');
271
+ const camera = document.getElementById('camera');
272
+ const canvas = document.getElementById('mainCanvas');
273
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
274
+ const overlayLayer = document.getElementById('overlay-layer');
275
+ const propsPanel = document.getElementById('propertiesPanel');
276
+ const dynProps = document.getElementById('dynamicProperties');
277
+
278
+ let currentImage = null;
279
+ let currentTool = 'select'; // select, erase, replaceColor, removeColor, canny
280
+
281
+ // --- History ---
282
+ let history = []; let historyStep = -1;
283
+ function saveState() {
284
+ if(!currentImage) return;
285
+ historyStep++; history.length = historyStep;
286
+ history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
287
+ }
288
+ function undo() { if (historyStep > 0) { historyStep--; ctx.putImageData(history[historyStep], 0, 0); } }
289
+ function redo() { if (historyStep < history.length - 1) { historyStep++; ctx.putImageData(history[historyStep], 0, 0); } }
290
+
291
+ // --- Loading System ---
292
+ function showLoading(msg = "PROCESSING...") {
293
+ document.getElementById('loaderText').innerText = msg;
294
+ document.getElementById('globalLoader').style.display = 'flex';
295
+ }
296
+ function hideLoading() { document.getElementById('globalLoader').style.display = 'none'; }
297
+ function openModal(id) { document.getElementById(id).style.display = 'flex'; }
298
+ function closeModal(id) { document.getElementById(id).style.display = 'none'; }
299
+ // --- Color Picker Logic (iro.js) ---
300
+ let iroPicker = new iro.ColorPicker("#pickerContainer", {
301
+ width: 200, color: "#ff0000",
302
+ layoutDirection: "vertical",
303
+ layout: [
304
+ { component: iro.ui.Wheel, options: {} },
305
+ { component: iro.ui.Slider, options: { sliderType: 'value' } },
306
+ { component: iro.ui.Slider, options: { sliderType: 'alpha' } } // Transparency slider
307
+ ]
308
+ });
309
+
310
+ // Current target to update when color changes (e.g., 'replacementColor', 'textColorActive')
311
+ let activeColorTarget = null;
312
+ let customReplacementColor = {r: 255, g: 0, b: 0, a: 255};
313
+
314
+ iroPicker.on('color:change', function(color) {
315
+ document.getElementById('hexInput').value = color.hexString;
316
+ document.getElementById('rgbInput').value = `rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})`;
317
+
318
+ if(activeColorTarget === 'replacement') {
319
+ customReplacementColor = {r: color.rgb.r, g: color.rgb.g, b: color.rgb.b, a: Math.round(color.alpha * 255)};
320
+ document.getElementById('replaceColorPreview').style.backgroundColor = color.hex8String;
321
+ } else if (activeColorTarget === 'text-solid' && activeDOMObject) {
322
+ activeDOMObject.querySelector('.editable-text').style.color = color.hex8String;
323
+ } else if (activeColorTarget === 'text-grad-1' && activeDOMObject) {
324
+ updateTextGradient(activeDOMObject, 1, color.hex8String);
325
+ } else if (activeColorTarget === 'text-grad-2' && activeDOMObject) {
326
+ updateTextGradient(activeDOMObject, 2, color.hex8String);
327
+ }
328
+ });
329
+
330
+ // Input syncing
331
+ document.getElementById('hexInput').addEventListener('change', (e) => iroPicker.color.hexString = e.target.value);
332
+
333
+ function openColorPicker(target, buttonEl) {
334
+ activeColorTarget = target;
335
+ const ui = document.getElementById('colorPickerUI');
336
+ const rect = buttonEl.getBoundingClientRect();
337
+ ui.style.display = 'block';
338
+ ui.style.top = rect.top + 'px';
339
+ ui.style.right = '280px'; // Pop out left of properties panel
340
+ ui.style.left = 'auto';
341
+ }
342
+ function closeColorPicker() { document.getElementById('colorPickerUI').style.display = 'none'; }
343
+
344
+ let isPickingColor = false;
345
+ function activateEyedropper() {
346
+ if (!window.EyeDropper) { alert("Your browser doesn't support the native Eyedropper API. Click on the canvas instead."); return; }
347
+ const eyeDropper = new EyeDropper();
348
+ eyeDropper.open().then(result => {
349
+ iroPicker.color.hexString = result.sRGBHex;
350
+ }).catch(e => console.log(e));
351
+ }
352
+
353
+ // --- Camera System (Pan & Zoom) ---
354
+ let scale = 1, panX = 0, panY = 0;
355
+ let isPanning = false, startPanX, startPanY;
356
+
357
+ function updateCamera() {
358
+ camera.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
359
+ document.getElementById('zoomLevelIndicator').innerText = Math.round(scale * 100) + '%';
360
+ }
361
+
362
+ function generateEdgeMap() {
363
+ if (!currentImage) return;
364
+ const eCanvas = document.getElementById('edgeOverlay');
365
+ eCanvas.width = canvas.width; eCanvas.height = canvas.height;
366
+ const eCtx = eCanvas.getContext('2d');
367
+
368
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
369
+ const data = imgData.data;
370
+ const edgeData = eCtx.createImageData(canvas.width, canvas.height);
371
+ const eData = edgeData.data;
372
+
373
+ // Fast boundary detection
374
+ for (let y = 0; y < canvas.height - 1; y++) {
375
+ for (let x = 0; x < canvas.width - 1; x++) {
376
+ let idx = (y * canvas.width + x) * 4;
377
+ let rightIdx = (y * canvas.width + (x + 1)) * 4;
378
+ let bottomIdx = ((y + 1) * canvas.width + x) * 4;
379
+
380
+ if (data[idx+3] === 0) continue; // Skip already transparent areas
381
+
382
+ // Check difference with neighbor pixels
383
+ let diffRight = Math.abs(data[idx]-data[rightIdx]) + Math.abs(data[idx+1]-data[rightIdx+1]) + Math.abs(data[idx+2]-data[rightIdx+2]);
384
+ let diffBottom = Math.abs(data[idx]-data[bottomIdx]) + Math.abs(data[idx+1]-data[bottomIdx+1]) + Math.abs(data[idx+2]-data[bottomIdx+2]);
385
+
386
+ // If color difference is greater than tolerance, draw an outline pixel
387
+ if (diffRight > colorTolerance || diffBottom > colorTolerance) {
388
+ eData[idx] = 99; // Logic Spine Indigo R
389
+ eData[idx+1] = 102; // G
390
+ eData[idx+2] = 241; // B
391
+ eData[idx+3] = 255; // Solid Alpha
392
+ }
393
+ }
394
+ }
395
+ eCtx.putImageData(edgeData, 0, 0);
396
+ }
397
+
398
+ function clearEdgeMap() {
399
+ const eCanvas = document.getElementById('edgeOverlay');
400
+ if (eCanvas) {
401
+ const eCtx = eCanvas.getContext('2d');
402
+ eCtx.clearRect(0, 0, eCanvas.width || 10000, eCanvas.height || 10000);
403
+ }
404
+ }
405
+
406
+ function resetCamera() {
407
+ if(!currentImage) return;
408
+ const vRect = viewport.getBoundingClientRect();
409
+ // Fit to screen with some padding
410
+ const scaleX = (vRect.width - 100) / canvas.width;
411
+ const scaleY = (vRect.height - 100) / canvas.height;
412
+ scale = Math.min(scaleX, scaleY, 1);
413
+ panX = (vRect.width - (canvas.width * scale)) / 2;
414
+ panY = (vRect.height - (canvas.height * scale)) / 2;
415
+ updateCamera();
416
+ }
417
+
418
+ // Math to convert screen mouse coordinates to raw Canvas coordinates (crucial for accurate drawing when zoomed)
419
+ function getCanvasCoords(clientX, clientY) {
420
+ const vRect = viewport.getBoundingClientRect();
421
+ const mouseX = clientX - vRect.left;
422
+ const mouseY = clientY - vRect.top;
423
+ return {
424
+ x: (mouseX - panX) / scale,
425
+ y: (mouseY - panY) / scale
426
+ };
427
+ }
428
+
429
+ // Mouse Wheel Zoom
430
+ viewport.addEventListener('wheel', (e) => {
431
+ e.preventDefault();
432
+ const zoomDirection = e.deltaY < 0 ? 1.1 : 0.9;
433
+ const vRect = viewport.getBoundingClientRect();
434
+ const mouseX = e.clientX - vRect.left;
435
+ const mouseY = e.clientY - vRect.top;
436
+
437
+ const newScale = Math.max(0.1, Math.min(scale * zoomDirection, 20)); // Allow 2000% zoom
438
+
439
+ // Math to keep the pixel under the mouse in the same screen spot
440
+ panX = mouseX - (mouseX - panX) * (newScale / scale);
441
+ panY = mouseY - (mouseY - panY) * (newScale / scale);
442
+ scale = newScale;
443
+ updateCamera();
444
+ }, { passive: false });
445
+
446
+ // Pan with Right Click or Middle Click
447
+ viewport.addEventListener('mousedown', (e) => {
448
+ if (e.button === 1 || e.button === 2) {
449
+ isPanning = true; startPanX = e.clientX - panX; startPanY = e.clientY - panY;
450
+ viewport.style.cursor = 'grabbing';
451
+ }
452
+ });
453
+ window.addEventListener('mousemove', (e) => {
454
+ if (isPanning) { panX = e.clientX - startPanX; panY = e.clientY - startPanY; updateCamera(); }
455
+ });
456
+ window.addEventListener('mouseup', (e) => {
457
+ if (isPanning) { isPanning = false; viewport.style.cursor = currentTool === 'select' ? 'grab' : 'crosshair'; }
458
+ });
459
+
460
+ // Touch gestures for Mobile (Pinch zoom & drag pan)
461
+ let initialPinchDistance = null; let initialScale = 1;
462
+ viewport.addEventListener('touchstart', (e) => {
463
+ if(e.touches.length === 2) {
464
+ initialPinchDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
465
+ initialScale = scale;
466
+ } else if (e.touches.length === 1 && currentTool === 'select') {
467
+ isPanning = true; startPanX = e.touches[0].clientX - panX; startPanY = e.touches[0].clientY - panY;
468
+ }
469
+ });
470
+ viewport.addEventListener('touchmove', (e) => {
471
+ if(e.touches.length === 2 && initialPinchDistance) {
472
+ e.preventDefault();
473
+ const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
474
+ const zoomDir = dist / initialPinchDistance;
475
+ // Simple zoom relative to center for mobile to save complexity
476
+ const newScale = Math.max(0.1, Math.min(initialScale * zoomDir, 20));
477
+
478
+ const vRect = viewport.getBoundingClientRect();
479
+ const centerX = vRect.width / 2; const centerY = vRect.height / 2;
480
+ panX = centerX - (centerX - panX) * (newScale / scale);
481
+ panY = centerY - (centerY - panY) * (newScale / scale);
482
+ scale = newScale; updateCamera();
483
+ } else if (e.touches.length === 1 && isPanning) {
484
+ panX = e.touches[0].clientX - startPanX; panY = e.touches[0].clientY - startPanY; updateCamera();
485
+ }
486
+ }, { passive: false });
487
+ viewport.addEventListener('touchend', () => { initialPinchDistance = null; isPanning = false; });
488
+
489
+
490
+ // --- UI & Tools ---
491
+ let brushSize = 20; let colorTolerance = 30;
492
+ function setTool(toolName) {
493
+ currentTool = toolName;
494
+ document.querySelectorAll('.tool-btn').forEach(btn => {
495
+ btn.classList.remove('text-indigo-400'); btn.classList.add('text-neutral-400');
496
+ if(btn.dataset.tool === toolName) { btn.classList.remove('text-neutral-400'); btn.classList.add('text-indigo-400'); }
497
+ });
498
+
499
+ viewport.className = toolName === 'select' ? 'flex-1 cursor-grab' : 'flex-1 drawing-mode';
500
+ propsPanel.style.display = 'flex'; propsPanel.classList.remove('hidden');
501
+
502
+ // NEW: Handle the Live Edge Overlay
503
+ if (toolName === 'canny' && currentImage) {
504
+ showLoading("MAPPING EDGES...");
505
+ setTimeout(() => { generateEdgeMap(); hideLoading(); }, 50);
506
+ } else {
507
+ clearEdgeMap();
508
+ }
509
+
510
+ if(toolName === 'erase') {
511
+ dynProps.innerHTML = `
512
+ <label class="text-sm text-neutral-400">Brush Size</label>
513
+ <input type="range" min="1" max="200" value="${brushSize}" onchange="brushSize = this.value" class="w-full accent-indigo-500">
514
+ `;
515
+ } else if (toolName === 'replaceColor' || toolName === 'removeColor' || toolName === 'canny') {
516
+ let replaceBtn = toolName === 'replaceColor' ? `
517
+ <label class="text-sm text-neutral-400 mt-4">Replace With</label>
518
+ <button id="replaceColorPreview" onclick="openColorPicker('replacement', this)" class="w-full h-10 rounded border border-neutral-700 mt-1" style="background: red;"></button>
519
+ ` : '';
520
+
521
+ let liveUpdate = toolName === 'canny' ? `oninput="colorTolerance = parseInt(this.value); generateEdgeMap();"` : `onchange="colorTolerance = parseInt(this.value)"`;
522
+
523
+ // Generate the Palette HTML
524
+ let paletteHtml = '<div class="grid grid-cols-4 gap-2 mt-2">';
525
+ if (currentImage) {
526
+ const colors = getDominantColors();
527
+ colors.forEach(c => {
528
+ paletteHtml += `<button onclick="setTargetColorFromPalette(${c[0]}, ${c[1]}, ${c[2]})" class="w-full h-8 rounded border border-neutral-700 hover:scale-110 transition-transform" style="background: rgb(${c[0]},${c[1]},${c[2]})"></button>`;
529
+ });
530
+ }
531
+ paletteHtml += '</div>';
532
+
533
+ dynProps.innerHTML = `
534
+ <label class="text-sm text-neutral-400">Dominant Colors</label>
535
+ ${paletteHtml}
536
+ <p class="text-[10px] text-neutral-500 mt-1 mb-4">Click a swatch to target it, or click the image manually.</p>
537
+
538
+ <label class="text-sm text-neutral-400">Tolerance</label>
539
+ <input type="range" min="0" max="255" value="${colorTolerance}" ${liveUpdate} class="w-full accent-indigo-500">
540
+ ${replaceBtn}
541
+ `;
542
+ } else { propsPanel.style.display = 'none'; }
543
+ }
544
+
545
+ // --- Drawing & Image Processing ---
546
+ function loadImage(e) {
547
+ const file = e.target.files[0]; if (!file) return;
548
+ showLoading("LOADING IMAGE...");
549
+ const reader = new FileReader();
550
+ reader.onload = function(event) {
551
+ const img = new Image();
552
+ img.onload = function() {
553
+ canvas.width = img.width; canvas.height = img.height;
554
+ ctx.drawImage(img, 0, 0);
555
+ currentImage = img;
556
+ camera.style.width = img.width + 'px'; camera.style.height = img.height + 'px';
557
+ history = []; historyStep = -1; saveState();
558
+ resetCamera(); setTool('select'); hideLoading();
559
+ }
560
+ img.src = event.target.result;
561
+ }
562
+ reader.readAsDataURL(file);
563
+ }
564
+
565
+ let isDrawing = false;
566
+ viewport.addEventListener('mousedown', (e) => {
567
+ if(!currentImage || e.button !== 0 || currentTool === 'select' || e.target.closest('.draggable-obj')) return;
568
+ const coords = getCanvasCoords(e.clientX, e.clientY);
569
+
570
+ if (currentTool === 'erase') {
571
+ isDrawing = true; ctx.globalCompositeOperation = 'destination-out';
572
+ ctx.beginPath(); ctx.arc(coords.x, coords.y, brushSize / 2, 0, Math.PI * 2); ctx.fill();
573
+ } else if (currentTool === 'removeColor' || currentTool === 'replaceColor') {
574
+ showLoading("PROCESSING PIXELS...");
575
+ // Use setTimeout to allow UI to render loading screen before heavy CPU task
576
+ setTimeout(() => {
577
+ processGlobalColor(coords.x, coords.y, currentTool === 'removeColor' ? 'remove' : 'replace');
578
+ hideLoading();
579
+ }, 50);
580
+ } else if (currentTool === 'canny') {
581
+ showLoading("CALCULATING AREA...");
582
+ setTimeout(() => { floodFill(Math.floor(coords.x), Math.floor(coords.y)); hideLoading(); }, 50);
583
+ }
584
+ });
585
+
586
+ viewport.addEventListener('mousemove', (e) => {
587
+ if(!isDrawing || currentTool !== 'erase') return;
588
+ const coords = getCanvasCoords(e.clientX, e.clientY);
589
+ ctx.lineTo(coords.x, coords.y);
590
+ ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.stroke();
591
+ ctx.beginPath(); ctx.moveTo(coords.x, coords.y);
592
+ });
593
+
594
+ window.addEventListener('mouseup', () => { if(isDrawing) { isDrawing = false; ctx.globalCompositeOperation = 'source-over'; saveState(); } });
595
+ // --- Color Palette Extractor ---
596
+ function getDominantColors() {
597
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
598
+ const colorCounts = {};
599
+
600
+ // Scan every 4th pixel to save CPU time (fast processing)
601
+ for (let i = 0; i < imgData.length; i += 16) {
602
+ if (imgData[i+3] === 0) continue; // Skip transparent
603
+
604
+ // Round to nearest 15 to group very similar shades together
605
+ const r = Math.round(imgData[i] / 15) * 15;
606
+ const g = Math.round(imgData[i+1] / 15) * 15;
607
+ const b = Math.round(imgData[i+2] / 15) * 15;
608
+ const rgb = `${r},${g},${b}`;
609
+
610
+ colorCounts[rgb] = (colorCounts[rgb] || 0) + 1;
611
+ }
612
+
613
+ // Sort by most used and grab the top 12 colors
614
+ return Object.entries(colorCounts)
615
+ .sort((a, b) => b[1] - a[1])
616
+ .slice(0, 12)
617
+ .map(e => e[0].split(',').map(Number));
618
+ }
619
+
620
+ // Global function for the UI buttons to set the target color
621
+ window.setTargetColorFromPalette = function(r, g, b) {
622
+ // Simulate a click on the canvas to set the target for the Replace/Remove tools
623
+ targetColor = {r: r, g: g, b: b, a: 255};
624
+ alert(`Target color set to RGB(${r}, ${g}, ${b}). Now click the canvas to apply!`);
625
+ };
626
+ // --- Pixel Algorithms ---
627
+ function getPixelColor(x, y, imgData) {
628
+ const idx = (Math.floor(y) * canvas.width + Math.floor(x)) * 4;
629
+ return { r: imgData.data[idx], g: imgData.data[idx+1], b: imgData.data[idx+2], a: imgData.data[idx+3] };
630
+ }
631
+ function colorMatch(c1, c2, tol) { return Math.abs(c1.r - c2.r) <= tol && Math.abs(c1.g - c2.g) <= tol && Math.abs(c1.b - c2.b) <= tol && c1.a > 0; }
632
+
633
+ function processGlobalColor(x, y, mode) {
634
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
635
+ const target = getPixelColor(x, y, imgData);
636
+ for (let i = 0; i < imgData.data.length; i += 4) {
637
+ let current = {r: imgData.data[i], g: imgData.data[i+1], b: imgData.data[i+2], a: imgData.data[i+3]};
638
+ if (colorMatch(current, target, colorTolerance)) {
639
+ if (mode === 'remove') imgData.data[i+3] = 0;
640
+ else { imgData.data[i] = customReplacementColor.r; imgData.data[i+1] = customReplacementColor.g; imgData.data[i+2] = customReplacementColor.b; imgData.data[i+3] = customReplacementColor.a; }
641
+ }
642
+ }
643
+ ctx.putImageData(imgData, 0, 0); saveState();
644
+ }
645
+
646
+ function floodFill(startX, startY) {
647
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
648
+ const w = canvas.width; const h = canvas.height;
649
+ const target = getPixelColor(startX, startY, imgData);
650
+ if(target.a === 0) return; // Don't fill empty space
651
+
652
+ const stack = [[startX, startY]];
653
+ const getIdx = (x, y) => (y * w + x) * 4;
654
+
655
+ while(stack.length) {
656
+ let [x, y] = stack.pop(); let pixelPos = getIdx(x, y);
657
+ while(y-- >= 0 && colorMatch(getPixelColor(x, y, imgData), target, colorTolerance)) pixelPos -= w * 4;
658
+ pixelPos += w * 4; ++y;
659
+ let reachLeft = false; let reachRight = false;
660
+
661
+ while(y++ < h - 1 && colorMatch(getPixelColor(x, y, imgData), target, colorTolerance)) {
662
+ imgData.data[pixelPos + 3] = 0; // Erase to transparent
663
+
664
+ if(x > 0) {
665
+ if(colorMatch(getPixelColor(x - 1, y, imgData), target, colorTolerance)) {
666
+ if(!reachLeft) { stack.push([x - 1, y]); reachLeft = true; }
667
+ } else reachLeft = false;
668
+ }
669
+ if(x < w - 1) {
670
+ if(colorMatch(getPixelColor(x + 1, y, imgData), target, colorTolerance)) {
671
+ if(!reachRight) { stack.push([x + 1, y]); reachRight = true; }
672
+ } else reachRight = false;
673
+ }
674
+ pixelPos += w * 4;
675
+ }
676
+ }
677
+ ctx.putImageData(imgData, 0, 0);
678
+ saveState();
679
+
680
+ // Re-run the edge map so the preview updates immediately after the cut!
681
+ generateEdgeMap();
682
+ }
683
+
684
+ // --- DOM Objects (Text & Images) with Rotate & Resize ---
685
+ let activeDOMObject = null;
686
+ let objCounter = 0;
687
+
688
+ function showTextProperties(el) {
689
+ propsPanel.style.display = 'flex';
690
+ dynProps.innerHTML = `
691
+ <label class="text-sm text-neutral-400">Color Style</label>
692
+ <select id="colorStyleSel" class="w-full bg-neutral-800 border border-neutral-700 rounded p-2 text-white outline-none mb-2" onchange="toggleGradientUI(this.value)">
693
+ <option value="solid">Solid Color</option>
694
+ <option value="gradient">Linear Gradient</option>
695
+ </select>
696
+
697
+ <div id="solidUI">
698
+ <button onclick="openColorPicker('text-solid', this)" class="w-full h-8 rounded border border-neutral-700" style="background: white;"></button>
699
+ </div>
700
+
701
+ <div id="gradientUI" class="hidden">
702
+ <div class="flex gap-2">
703
+ <button onclick="openColorPicker('text-grad-1', this)" class="w-full h-8 rounded border border-neutral-700" style="background: #ff0000;"></button>
704
+ <button onclick="openColorPicker('text-grad-2', this)" class="w-full h-8 rounded border border-neutral-700" style="background: #0000ff;"></button>
705
+ </div>
706
+ <label class="text-xs text-neutral-400 mt-2">Direction</label>
707
+ <select id="gradDirSel" onchange="updateTextGradientActive()" class="w-full bg-neutral-800 border border-neutral-700 rounded p-1 text-white text-xs">
708
+ <option value="to right">Horizontal</option>
709
+ <option value="to bottom">Vertical</option>
710
+ <option value="to bottom right">Diagonal</option>
711
+ </select>
712
+ </div>
713
+
714
+ <label class="text-sm text-neutral-400 mt-4">Font Size</label>
715
+ <input type="range" min="10" max="500" value="60" oninput="activeDOMObject.querySelector('.editable-text').style.fontSize = this.value + 'px'" class="w-full accent-indigo-500">
716
+ <button onclick="activeDOMObject.remove(); propsPanel.style.display='none'" class="mt-4 w-full py-2 bg-red-900/50 hover:bg-red-800 text-red-400 rounded transition-colors text-sm"><i class="ph ph-trash"></i> Delete Object</button>
717
+ `;
718
+ }
719
+
720
+ // Expose function for the select menu
721
+ window.toggleGradientUI = function(val) {
722
+ document.getElementById('solidUI').style.display = val === 'solid' ? 'block' : 'none';
723
+ document.getElementById('gradientUI').style.display = val === 'gradient' ? 'block' : 'none';
724
+ if(val === 'solid') {
725
+ activeDOMObject.querySelector('.editable-text').classList.remove('gradient-text');
726
+ activeDOMObject.querySelector('.editable-text').style.background = 'none';
727
+ } else {
728
+ activeDOMObject.querySelector('.editable-text').classList.add('gradient-text');
729
+ updateTextGradientActive();
730
+ }
731
+ };
732
+
733
+ window.updateTextGradientActive = function() {
734
+ if(!activeDOMObject) return;
735
+ const dir = document.getElementById('gradDirSel').value;
736
+ // Get colors from buttons (simplified for this demo)
737
+ const c1 = document.querySelectorAll('#gradientUI button')[0].style.backgroundColor;
738
+ const c2 = document.querySelectorAll('#gradientUI button')[1].style.backgroundColor;
739
+ updateTextGradient(activeDOMObject, null, null, `${dir}, ${c1}, ${c2}`);
740
+ }
741
+
742
+ function updateTextGradient(obj, num, hex, fullCssString) {
743
+ const txt = obj.querySelector('.editable-text');
744
+ if(fullCssString) {
745
+ txt.style.backgroundImage = `linear-gradient(${fullCssString})`;
746
+ } else {
747
+ // Hacking it slightly to avoid complex state management for this demo
748
+ txt.style.backgroundImage = `linear-gradient(to right, ${num===1?hex:'red'}, ${num===2?hex:'blue'})`;
749
+ }
750
+ }
751
+
752
+ function makeTransformable(el, type) {
753
+ let isDragging = false, isResizing = false, isRotating = false;
754
+ let startX, startY, initX, initY, initW, initH;
755
+ let centerX, centerY, currentRotation = 0;
756
+
757
+ const handleResize = el.querySelector('.resize-handle');
758
+ const handleRotate = el.querySelector('.rotate-handle');
759
+
760
+ el.addEventListener('mousedown', (e) => {
761
+ if(currentTool !== 'select') return;
762
+ e.stopPropagation(); // Stop pan
763
+ document.querySelectorAll('.draggable-obj').forEach(obj => obj.classList.remove('active'));
764
+ el.classList.add('active'); activeDOMObject = el;
765
+ closeColorPicker();
766
+
767
+ if (type === 'text') showTextProperties(el);
768
+
769
+ if(e.target === handleResize) { isResizing = true; startX = e.clientX; startY = e.clientY; initW = el.offsetWidth; initH = el.offsetHeight; return; }
770
+ if(e.target === handleRotate) {
771
+ isRotating = true;
772
+ const rect = el.getBoundingClientRect();
773
+ centerX = rect.left + rect.width / 2; centerY = rect.top + rect.height / 2;
774
+ return;
775
+ }
776
+
777
+ isDragging = true; startX = e.clientX; startY = e.clientY;
778
+ // Parse translate if it exists
779
+ const transform = el.style.transform;
780
+ const matchX = transform.match(/translateX\(([-\d.]+)px\)/);
781
+ const matchY = transform.match(/translateY\(([-\d.]+)px\)/);
782
+ initX = matchX ? parseFloat(matchX[1]) : 0;
783
+ initY = matchY ? parseFloat(matchY[1]) : 0;
784
+ });
785
+
786
+ window.addEventListener('mousemove', (e) => {
787
+ if(isDragging) {
788
+ const dx = (e.clientX - startX) / scale; // Account for camera scale
789
+ const dy = (e.clientY - startY) / scale;
790
+ el.style.transform = `translateX(${initX + dx}px) translateY(${initY + dy}px) rotate(${currentRotation}deg)`;
791
+ } else if (isResizing && type === 'image') {
792
+ // Only image resizes physically, text resizes via font-size
793
+ const dx = (e.clientX - startX) / scale;
794
+ el.querySelector('img').style.width = Math.max(50, initW + dx) + 'px';
795
+ } else if (isRotating) {
796
+ const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
797
+ currentRotation = angle * (180 / Math.PI) + 90; // Offset by 90deg because handle is at top
798
+ // Extract current translate
799
+ const transform = el.style.transform;
800
+ const matchX = transform.match(/translateX\(([-\d.]+)px\)/) || [0,0];
801
+ const matchY = transform.match(/translateY\(([-\d.]+)px\)/) || [0,0];
802
+ el.style.transform = `translateX(${matchX[1]}px) translateY(${matchY[1]}px) rotate(${currentRotation}deg)`;
803
+ }
804
+ });
805
+
806
+ window.addEventListener('mouseup', () => { isDragging = false; isResizing = false; isRotating = false; });
807
+ }
808
+
809
+ function addTextObject() {
810
+ setTool('select');
811
+ const div = document.createElement('div');
812
+ div.className = 'draggable-obj active';
813
+ // Start centered in current view
814
+ div.style.transform = `translateX(${-panX/scale + 100}px) translateY(${-panY/scale + 100}px) rotate(0deg)`;
815
+
816
+ const txt = document.createElement('div');
817
+ txt.className = 'editable-text';
818
+ txt.contentEditable = true;
819
+ txt.innerText = "Double Click to Edit";
820
+ txt.style.fontSize = '60px';
821
+ txt.style.color = '#ffffff';
822
+
823
+ div.innerHTML = `<div class="handle rotate-handle"></div>`;
824
+ div.appendChild(txt);
825
+ overlayLayer.appendChild(div);
826
+ activeDOMObject = div;
827
+ makeTransformable(div, 'text');
828
+ showTextProperties(div);
829
+ }
830
+
831
+ function addOverlayImage(e) {
832
+ setTool('select');
833
+ const file = e.target.files[0]; if (!file) return;
834
+ const reader = new FileReader();
835
+ reader.onload = function(event) {
836
+ const div = document.createElement('div');
837
+ div.className = 'draggable-obj active';
838
+ div.style.transform = `translateX(${-panX/scale + 100}px) translateY(${-panY/scale + 100}px) rotate(0deg)`;
839
+
840
+ div.innerHTML = `
841
+ <div class="handle rotate-handle"></div>
842
+ <img src="${event.target.result}" style="width: 300px; display: block;" draggable="false">
843
+ <div class="handle resize-handle"></div>
844
+ `;
845
+ overlayLayer.appendChild(div);
846
+ activeDOMObject = div;
847
+ makeTransformable(div, 'image');
848
+
849
+ // Show simple delete props
850
+ propsPanel.style.display = 'flex';
851
+ dynProps.innerHTML = `<button onclick="activeDOMObject.remove(); propsPanel.style.display='none'" class="mt-4 w-full py-2 bg-red-900/50 hover:bg-red-800 text-red-400 rounded text-sm"><i class="ph ph-trash"></i> Delete Image</button>`;
852
+ }
853
+ reader.readAsDataURL(file);
854
+ }
855
+
856
+ async function executeVectorize() {
857
+ closeModal('vectorModal');
858
+ const quality = document.getElementById('vectorQuality').value;
859
+
860
+ // 1. Get the flattened image
861
+ const blob = await getFlattenedImageBlob();
862
+
863
+ // 2. Prepare the payload
864
+ const formData = new FormData();
865
+ formData.append('file', blob, 'logicspine_workspace.png');
866
+ formData.append('quality', quality);
867
+
868
+ showLoading(`VECTORIZING (${quality.toUpperCase()})...`);
869
+
870
+ try {
871
+ // 3. Send to FastAPI
872
+ const response = await fetch('https://5m4ck3r-polypath.hf.space/vectorize/', {
873
+ method: 'POST',
874
+ headers: {
875
+ 'X-LogicSpine-Key': 'LogicSpine_PolyPath_Secure_2026!'
876
+ },
877
+ body: formData
878
+ });
879
+
880
+ if (!response.ok) throw new Error("Backend failed");
881
+
882
+ // 4. Download the massive SVG directly to the browser
883
+ const blobRes = await response.blob();
884
+ const url = window.URL.createObjectURL(blobRes);
885
+ const a = document.createElement('a');
886
+ a.href = url;
887
+ a.download = `LogicSpine_${quality}_vector.svg`;
888
+ a.click();
889
+ } catch (e) {
890
+ alert("Error connecting to Python backend. Is uvicorn running?");
891
+ console.error(e);
892
+ }
893
+ hideLoading();
894
+ }
895
+
896
+ async function executeExport() {
897
+ closeModal('exportModal');
898
+ let preset = document.getElementById('exportPreset').value;
899
+ let width = preset === 'custom' ? document.getElementById('customWidth').value : preset;
900
+
901
+ if (width === 'original') width = canvas.width;
902
+
903
+ // 1. Get the flattened image
904
+ const blob = await getFlattenedImageBlob();
905
+
906
+ // 2. Prepare the payload
907
+ const formData = new FormData();
908
+ formData.append('file', blob, 'logicspine_workspace.png');
909
+ formData.append('width', width);
910
+
911
+ showLoading(`UPSCALING TO ${width}px...`);
912
+
913
+ try {
914
+ // 3. Send to FastAPI
915
+ const response = await fetch('https://5m4ck3r-polypath.hf.space/export-uhd/', {
916
+ method: 'POST',
917
+ headers: {
918
+ 'X-LogicSpine-Key': 'LogicSpine_PolyPath_Secure_2026!'
919
+ },
920
+ body: formData
921
+ });
922
+
923
+ if (!response.ok) throw new Error("Backend failed");
924
+
925
+ // 4. Download the ultra-high-res PNG
926
+ const blobRes = await response.blob();
927
+ const url = window.URL.createObjectURL(blobRes);
928
+ const a = document.createElement('a');
929
+ a.href = url;
930
+ a.download = `LogicSpine_UHD_${width}px.png`;
931
+ a.click();
932
+ } catch (e) {
933
+ alert("Error connecting to Python backend. Is uvicorn running?");
934
+ console.error(e);
935
+ }
936
+ hideLoading();
937
+ }
938
+
939
+ // --- API Integration & Layer Flattening ---
940
+ async function getFlattenedImageBlob() {
941
+ showLoading("FLATTENING LAYERS...");
942
+
943
+ // 1. Hide UI elements
944
+ document.querySelectorAll('.active').forEach(el => el.classList.remove('active'));
945
+ clearEdgeMap();
946
+
947
+ // 2. Temporarily reset camera zoom
948
+ const currentTransform = camera.style.transform;
949
+ camera.style.transform = `translate(0px, 0px) scale(1)`;
950
+
951
+ const cameraDiv = document.getElementById('camera');
952
+
953
+ // THE FIX: Remove the glowing drop shadow temporarily so it doesn't bleed into the transparency!
954
+ const originalShadow = cameraDiv.style.boxShadow;
955
+ cameraDiv.style.boxShadow = 'none';
956
+
957
+ // 3. Take the snapshot
958
+ const renderedCanvas = await html2canvas(cameraDiv, {
959
+ backgroundColor: null, // Keeps the background completely transparent
960
+ scale: 1,
961
+ useCORS: true,
962
+ logging: false
963
+ });
964
+
965
+ // 4. Put the camera and shadow back where the user had it
966
+ cameraDiv.style.boxShadow = originalShadow;
967
+ camera.style.transform = currentTransform;
968
+ hideLoading();
969
+
970
+ // Convert to a file we can send to Python
971
+ return new Promise(resolve => renderedCanvas.toBlob(resolve, 'image/png'));
972
+ }
973
+
974
+ async function executePDFExport() {
975
+ closeModal('pdfModal');
976
+ const quality = document.getElementById('pdfQuality').value;
977
+ const width = document.getElementById('pdfWidth').value;
978
+
979
+ // 1. Get the flattened image of the workspace
980
+ const blob = await getFlattenedImageBlob();
981
+
982
+ // 2. Prepare the payload matching the Python backend requirements
983
+ const formData = new FormData();
984
+ formData.append('file', blob, 'logicspine_workspace.png');
985
+ formData.append('quality', quality);
986
+ formData.append('width', width);
987
+
988
+ showLoading(`GENERATING 3-PAGE PDF...`);
989
+
990
+ try {
991
+ // 3. Send to FastAPI
992
+ const response = await fetch('https://5m4ck3r-polypath.hf.space/export-pdf/', {
993
+ method: 'POST',
994
+ headers: {
995
+ 'X-LogicSpine-Key': 'LogicSpine_PolyPath_Secure_2026!'
996
+ },
997
+ body: formData
998
+ });
999
+
1000
+ if (!response.ok) throw new Error("Backend failed");
1001
+
1002
+ // 4. Download the final PDF file
1003
+ const blobRes = await response.blob();
1004
+ const url = window.URL.createObjectURL(blobRes);
1005
+ const a = document.createElement('a');
1006
+ a.href = url;
1007
+ a.download = `LogicSpine_Project_Report.pdf`;
1008
+ a.click();
1009
+ } catch (e) {
1010
+ alert("Error connecting to Python backend. Check terminal for errors.");
1011
+ console.error(e);
1012
+ }
1013
+ hideLoading();
1014
+ }
1015
+
1016
+ </script>
1017
+ </body>
1018
+ </html>