Spaces:
Running
Running
Update index.html
Browse files- index.html +1018 -19
index.html
CHANGED
|
@@ -1,19 +1,1018 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|