Spaces:
Running on Zero
Running on Zero
Update index.html
Browse files- index.html +281 -196
index.html
CHANGED
|
@@ -153,11 +153,15 @@ body{
|
|
| 153 |
padding: 24px;
|
| 154 |
}
|
| 155 |
|
| 156 |
-
|
| 157 |
position: relative;
|
| 158 |
-
display: inline-block;
|
| 159 |
max-width: 100%;
|
| 160 |
max-height: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
#draw-canvas {
|
|
@@ -171,7 +175,6 @@ body{
|
|
| 171 |
box-shadow: 0 12px 48px rgba(0,0,0,.9);
|
| 172 |
touch-action: none;
|
| 173 |
display: block;
|
| 174 |
-
z-index: 1;
|
| 175 |
}
|
| 176 |
#draw-canvas.cursor-brush { cursor: crosshair; }
|
| 177 |
#draw-canvas.cursor-eraser {
|
|
@@ -191,41 +194,65 @@ body{
|
|
| 191 |
display: none;
|
| 192 |
}
|
| 193 |
|
| 194 |
-
|
| 195 |
-
.draggable-asset {
|
| 196 |
position: absolute;
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
user-select: none;
|
| 199 |
touch-action: none;
|
| 200 |
-
z-index: 10;
|
| 201 |
border: 1px dashed transparent;
|
|
|
|
| 202 |
}
|
| 203 |
-
.
|
| 204 |
-
border-color:
|
| 205 |
}
|
| 206 |
-
.
|
| 207 |
-
pointer-events: none;
|
| 208 |
-
display: block;
|
| 209 |
width: 100%;
|
| 210 |
-
height:
|
|
|
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
-
.
|
| 213 |
position: absolute;
|
| 214 |
-
bottom: -6px;
|
| 215 |
-
right: -6px;
|
| 216 |
-
width: 12px;
|
| 217 |
-
height: 12px;
|
| 218 |
background: #fff;
|
| 219 |
-
border:
|
| 220 |
-
border-radius:
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
/* Loader */
|
| 226 |
.modern-loader{
|
| 227 |
display:none;position:absolute;inset:0;background:rgba(0,0,0,.92);
|
| 228 |
-
z-index:
|
| 229 |
}
|
| 230 |
.modern-loader.active{display:flex}
|
| 231 |
.loader-spinner{
|
|
@@ -443,7 +470,7 @@ body{
|
|
| 443 |
|
| 444 |
<!-- ββ Left icon rail ββ -->
|
| 445 |
<div class="rail">
|
| 446 |
-
<div class="rail-label">
|
| 447 |
<button class="rail-btn active" id="btn-brush" title="Brush">
|
| 448 |
<svg viewBox="0 0 24 24"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
|
| 449 |
</button>
|
|
@@ -453,19 +480,13 @@ body{
|
|
| 453 |
<div class="color-input-wrap">
|
| 454 |
<input type="color" id="color-picker" class="color-input" value="#000000" title="Brush Color">
|
| 455 |
</div>
|
|
|
|
| 456 |
<div class="size-slider-wrap">
|
| 457 |
<input type="range" id="size-slider" class="size-slider" min="1" max="50" value="5" title="Brush Size">
|
| 458 |
</div>
|
| 459 |
|
| 460 |
<div class="rail-sep"></div>
|
| 461 |
-
|
| 462 |
-
<button class="rail-btn" id="btn-upload-asset" title="Add Image/Asset">
|
| 463 |
-
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
| 464 |
-
</button>
|
| 465 |
-
<input type="file" id="asset-upload-input" accept="image/*" style="display:none;">
|
| 466 |
-
|
| 467 |
-
<div class="rail-sep"></div>
|
| 468 |
-
<div class="rail-label">Edit</div>
|
| 469 |
<button class="rail-btn" id="btn-undo" title="Undo">
|
| 470 |
<svg viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
|
| 471 |
</button>
|
|
@@ -476,6 +497,13 @@ body{
|
|
| 476 |
<svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
| 477 |
</button>
|
| 478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
<div class="rail-sep"></div>
|
| 480 |
<div class="rail-label">Result</div>
|
| 481 |
<button class="rail-btn" id="btn-download" title="Download Result" disabled>
|
|
@@ -490,7 +518,7 @@ body{
|
|
| 490 |
<button class="seg-btn active" id="seg-input">Sketch</button>
|
| 491 |
<button class="seg-btn" id="seg-result">Result</button>
|
| 492 |
</div>
|
| 493 |
-
<div class="canvas-meta" id="canvas-meta">Draw a sketch or add assets</div>
|
| 494 |
<div class="canvas-actions"></div>
|
| 495 |
</div>
|
| 496 |
|
|
@@ -501,12 +529,11 @@ body{
|
|
| 501 |
<div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
|
| 502 |
</div>
|
| 503 |
|
| 504 |
-
<div
|
| 505 |
-
|
| 506 |
-
|
|
|
|
| 507 |
</div>
|
| 508 |
-
|
| 509 |
-
<img id="result-img" alt="Result Image" style="display:none; aspect-ratio: 16 / 9;">
|
| 510 |
</div>
|
| 511 |
</div>
|
| 512 |
|
|
@@ -593,7 +620,7 @@ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.m
|
|
| 593 |
/* ββ DOM refs ββ */
|
| 594 |
const canvas = document.getElementById('draw-canvas');
|
| 595 |
const ctx = canvas.getContext('2d');
|
| 596 |
-
const
|
| 597 |
const resultImg = document.getElementById('result-img');
|
| 598 |
const btnDownload = document.getElementById('btn-download');
|
| 599 |
|
|
@@ -604,8 +631,9 @@ const btnRedo = document.getElementById('btn-redo');
|
|
| 604 |
const btnClear = document.getElementById('btn-clear');
|
| 605 |
const colorPicker = document.getElementById('color-picker');
|
| 606 |
const sizeSlider = document.getElementById('size-slider');
|
| 607 |
-
|
| 608 |
-
const
|
|
|
|
| 609 |
|
| 610 |
const promptInput = document.getElementById('custom-prompt-input');
|
| 611 |
const charCount = document.getElementById('char-count');
|
|
@@ -680,15 +708,16 @@ function updateCanvasRatio(ratio) {
|
|
| 680 |
canvas.width = w;
|
| 681 |
canvas.height = h;
|
| 682 |
canvas.style.aspectRatio = ratio.replace(":", " / ");
|
|
|
|
| 683 |
resultImg.style.aspectRatio = ratio.replace(":", " / ");
|
| 684 |
|
| 685 |
-
setupCanvas();
|
| 686 |
-
undoStack = [];
|
| 687 |
redoStack = [];
|
| 688 |
-
saveState();
|
| 689 |
|
| 690 |
-
// Clear assets to prevent
|
| 691 |
-
|
| 692 |
}
|
| 693 |
|
| 694 |
ratioSelect.addEventListener('change', (e) => {
|
|
@@ -715,6 +744,8 @@ function getPos(e) {
|
|
| 715 |
}
|
| 716 |
|
| 717 |
function startDrawing(e) {
|
|
|
|
|
|
|
| 718 |
e.preventDefault();
|
| 719 |
isDrawing = true;
|
| 720 |
const pos = getPos(e);
|
|
@@ -754,117 +785,12 @@ function stopDrawing() {
|
|
| 754 |
}
|
| 755 |
|
| 756 |
canvas.addEventListener('mousedown', startDrawing);
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
canvas.addEventListener('mouseout', stopDrawing);
|
| 760 |
|
| 761 |
canvas.addEventListener('touchstart', startDrawing);
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
/* ββ Asset Logic ββ */
|
| 766 |
-
btnUploadAsset.addEventListener('click', () => assetInput.click());
|
| 767 |
-
assetInput.addEventListener('change', (e) => {
|
| 768 |
-
const file = e.target.files[0];
|
| 769 |
-
if (file) {
|
| 770 |
-
const reader = new FileReader();
|
| 771 |
-
reader.onload = (ev) => addAsset(ev.target.result);
|
| 772 |
-
reader.readAsDataURL(file);
|
| 773 |
-
}
|
| 774 |
-
e.target.value = '';
|
| 775 |
-
});
|
| 776 |
-
|
| 777 |
-
function addAsset(src) {
|
| 778 |
-
const wrapper = document.createElement('div');
|
| 779 |
-
wrapper.className = 'draggable-asset';
|
| 780 |
-
|
| 781 |
-
const img = document.createElement('img');
|
| 782 |
-
img.src = src;
|
| 783 |
-
img.draggable = false;
|
| 784 |
-
|
| 785 |
-
const handle = document.createElement('div');
|
| 786 |
-
handle.className = 'resize-handle';
|
| 787 |
-
|
| 788 |
-
wrapper.appendChild(img);
|
| 789 |
-
wrapper.appendChild(handle);
|
| 790 |
-
|
| 791 |
-
img.style.width = '150px';
|
| 792 |
-
img.style.height = 'auto';
|
| 793 |
-
img.style.display = 'block';
|
| 794 |
-
|
| 795 |
-
// Center it initially
|
| 796 |
-
const wrapperRect = canvasWrapper.getBoundingClientRect();
|
| 797 |
-
const startLeft = wrapperRect.width / 2 - 75;
|
| 798 |
-
const startTop = wrapperRect.height / 2 - 75;
|
| 799 |
-
wrapper.style.left = startLeft + 'px';
|
| 800 |
-
wrapper.style.top = startTop + 'px';
|
| 801 |
-
|
| 802 |
-
canvasWrapper.appendChild(wrapper);
|
| 803 |
-
initDrag(wrapper);
|
| 804 |
-
showToast('Asset added. Double-click to delete.', 'info');
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
function initDrag(el) {
|
| 808 |
-
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
| 809 |
-
let isResizing = false;
|
| 810 |
-
let startX, startY, startW;
|
| 811 |
-
const handle = el.querySelector('.resize-handle');
|
| 812 |
-
|
| 813 |
-
el.onmousedown = dragMouseDown;
|
| 814 |
-
el.ontouchstart = (e) => { if(e.touches.length===1) dragMouseDown(e.touches[0]); };
|
| 815 |
-
|
| 816 |
-
function dragMouseDown(e) {
|
| 817 |
-
e.preventDefault();
|
| 818 |
-
if (e.target === handle) {
|
| 819 |
-
isResizing = true;
|
| 820 |
-
startX = e.clientX;
|
| 821 |
-
startW = el.offsetWidth;
|
| 822 |
-
document.onmousemove = resizeMove;
|
| 823 |
-
document.ontouchmove = (ev) => { if(ev.touches.length===1) resizeMove(ev.touches[0]); };
|
| 824 |
-
document.onmouseup = closeDragElement;
|
| 825 |
-
document.ontouchend = closeDragElement;
|
| 826 |
-
} else {
|
| 827 |
-
isResizing = false;
|
| 828 |
-
pos3 = e.clientX;
|
| 829 |
-
pos4 = e.clientY;
|
| 830 |
-
document.onmousemove = elementDrag;
|
| 831 |
-
document.ontouchmove = (ev) => { if(ev.touches.length===1) elementDrag(ev.touches[0]); };
|
| 832 |
-
document.onmouseup = closeDragElement;
|
| 833 |
-
document.ontouchend = closeDragElement;
|
| 834 |
-
}
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
function elementDrag(e) {
|
| 838 |
-
e.preventDefault();
|
| 839 |
-
pos1 = pos3 - e.clientX;
|
| 840 |
-
pos2 = pos4 - e.clientY;
|
| 841 |
-
pos3 = e.clientX;
|
| 842 |
-
pos4 = e.clientY;
|
| 843 |
-
el.style.top = (el.offsetTop - pos2) + "px";
|
| 844 |
-
el.style.left = (el.offsetLeft - pos1) + "px";
|
| 845 |
-
}
|
| 846 |
-
|
| 847 |
-
function resizeMove(e) {
|
| 848 |
-
e.preventDefault();
|
| 849 |
-
let newW = startW + (e.clientX - startX);
|
| 850 |
-
if (newW > 30) el.style.width = newW + 'px';
|
| 851 |
-
el.style.height = 'auto';
|
| 852 |
-
}
|
| 853 |
-
|
| 854 |
-
function closeDragElement() {
|
| 855 |
-
document.onmouseup = null;
|
| 856 |
-
document.onmousemove = null;
|
| 857 |
-
document.ontouchend = null;
|
| 858 |
-
document.ontouchmove = null;
|
| 859 |
-
isResizing = false;
|
| 860 |
-
}
|
| 861 |
-
|
| 862 |
-
// Double click to delete
|
| 863 |
-
el.ondblclick = () => {
|
| 864 |
-
el.remove();
|
| 865 |
-
showToast('Asset removed', 'info');
|
| 866 |
-
};
|
| 867 |
-
}
|
| 868 |
|
| 869 |
/* ββ Tool Listeners ββ */
|
| 870 |
btnBrush.addEventListener('click', () => {
|
|
@@ -899,8 +825,8 @@ sizeSlider.addEventListener('input', (e) => {
|
|
| 899 |
btnClear.addEventListener('click', () => {
|
| 900 |
setupCanvas();
|
| 901 |
saveState();
|
| 902 |
-
|
| 903 |
-
showToast('Canvas
|
| 904 |
});
|
| 905 |
|
| 906 |
btnUndo.addEventListener('click', async () => {
|
|
@@ -919,6 +845,196 @@ btnRedo.addEventListener('click', async () => {
|
|
| 919 |
}
|
| 920 |
});
|
| 921 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 922 |
btnDownload.addEventListener('click', () => {
|
| 923 |
if (resultImg.src && resultImg.src !== window.location.href) {
|
| 924 |
const a = document.createElement('a');
|
|
@@ -931,17 +1047,19 @@ btnDownload.addEventListener('click', () => {
|
|
| 931 |
/* ββ UI Toggles ββ */
|
| 932 |
function setViewMode(mode) {
|
| 933 |
if (mode === 'result') {
|
| 934 |
-
|
|
|
|
| 935 |
resultImg.style.display = 'block';
|
| 936 |
segResult.classList.add('active');
|
| 937 |
segInput.classList.remove('active');
|
| 938 |
canvasMeta.textContent = 'Generated Result';
|
| 939 |
} else {
|
| 940 |
-
|
|
|
|
| 941 |
resultImg.style.display = 'none';
|
| 942 |
segResult.classList.remove('active');
|
| 943 |
segInput.classList.add('active');
|
| 944 |
-
canvasMeta.textContent = 'Draw a sketch or add assets';
|
| 945 |
}
|
| 946 |
}
|
| 947 |
segInput.addEventListener('click', () => setViewMode('input'));
|
|
@@ -1063,39 +1181,6 @@ function setRunningUI(on) {
|
|
| 1063 |
cancelBtn.style.display = on ? 'flex' : 'none';
|
| 1064 |
}
|
| 1065 |
|
| 1066 |
-
async function getFlattenedDataURL() {
|
| 1067 |
-
// Create temp canvas to merge base sketch + assets
|
| 1068 |
-
const tempCanvas = document.createElement('canvas');
|
| 1069 |
-
tempCanvas.width = canvas.width;
|
| 1070 |
-
tempCanvas.height = canvas.height;
|
| 1071 |
-
const tempCtx = tempCanvas.getContext('2d');
|
| 1072 |
-
|
| 1073 |
-
tempCtx.fillStyle = '#ffffff';
|
| 1074 |
-
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
| 1075 |
-
tempCtx.drawImage(canvas, 0, 0);
|
| 1076 |
-
|
| 1077 |
-
const assets = document.querySelectorAll('.draggable-asset');
|
| 1078 |
-
const wrapperRect = canvasWrapper.getBoundingClientRect();
|
| 1079 |
-
const scaleX = canvas.width / wrapperRect.width;
|
| 1080 |
-
const scaleY = canvas.height / wrapperRect.height;
|
| 1081 |
-
|
| 1082 |
-
for (let asset of assets) {
|
| 1083 |
-
const img = asset.querySelector('img');
|
| 1084 |
-
const rect = asset.getBoundingClientRect();
|
| 1085 |
-
|
| 1086 |
-
const x = (rect.left - wrapperRect.left) * scaleX;
|
| 1087 |
-
const y = (rect.top - wrapperRect.top) * scaleY;
|
| 1088 |
-
const w = rect.width * scaleX;
|
| 1089 |
-
const h = rect.height * scaleY;
|
| 1090 |
-
|
| 1091 |
-
if (!img.complete) {
|
| 1092 |
-
await new Promise(resolve => { img.onload = resolve; });
|
| 1093 |
-
}
|
| 1094 |
-
tempCtx.drawImage(img, x, y, w, h);
|
| 1095 |
-
}
|
| 1096 |
-
return tempCanvas.toDataURL('image/png');
|
| 1097 |
-
}
|
| 1098 |
-
|
| 1099 |
async function runEdit() {
|
| 1100 |
if (running) return;
|
| 1101 |
|
|
@@ -1107,8 +1192,8 @@ async function runEdit() {
|
|
| 1107 |
showLoader('Submitting to queueβ¦');
|
| 1108 |
btnDownload.disabled = true;
|
| 1109 |
|
| 1110 |
-
//
|
| 1111 |
-
const sketch_b64 =
|
| 1112 |
const selectedRatio = ratioSelect.value;
|
| 1113 |
const [targetW, targetH] = dims[selectedRatio];
|
| 1114 |
|
|
@@ -1160,7 +1245,7 @@ async function runEdit() {
|
|
| 1160 |
}
|
| 1161 |
hideLoader('Done', 'done');
|
| 1162 |
showToast('Image generated successfully', 'info');
|
| 1163 |
-
btnDownload.disabled = false;
|
| 1164 |
} else {
|
| 1165 |
hideLoader('Done', 'done');
|
| 1166 |
}
|
|
|
|
| 153 |
padding: 24px;
|
| 154 |
}
|
| 155 |
|
| 156 |
+
#board-wrapper {
|
| 157 |
position: relative;
|
|
|
|
| 158 |
max-width: 100%;
|
| 159 |
max-height: 100%;
|
| 160 |
+
width: 100%;
|
| 161 |
+
height: 100%;
|
| 162 |
+
display: flex;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: center;
|
| 165 |
}
|
| 166 |
|
| 167 |
#draw-canvas {
|
|
|
|
| 175 |
box-shadow: 0 12px 48px rgba(0,0,0,.9);
|
| 176 |
touch-action: none;
|
| 177 |
display: block;
|
|
|
|
| 178 |
}
|
| 179 |
#draw-canvas.cursor-brush { cursor: crosshair; }
|
| 180 |
#draw-canvas.cursor-eraser {
|
|
|
|
| 194 |
display: none;
|
| 195 |
}
|
| 196 |
|
| 197 |
+
#asset-layer {
|
|
|
|
| 198 |
position: absolute;
|
| 199 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 200 |
+
pointer-events: none; /* Allows clicks to pass through to canvas */
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.asset-wrapper {
|
| 204 |
+
position: absolute;
|
| 205 |
+
pointer-events: auto;
|
| 206 |
user-select: none;
|
| 207 |
touch-action: none;
|
|
|
|
| 208 |
border: 1px dashed transparent;
|
| 209 |
+
transition: border-color 0.15s ease;
|
| 210 |
}
|
| 211 |
+
.asset-wrapper:hover, .asset-wrapper.active {
|
| 212 |
+
border-color: var(--accent2);
|
| 213 |
}
|
| 214 |
+
.asset-img {
|
|
|
|
|
|
|
| 215 |
width: 100%;
|
| 216 |
+
height: 100%;
|
| 217 |
+
object-fit: contain;
|
| 218 |
+
pointer-events: none;
|
| 219 |
+
background: transparent;
|
| 220 |
}
|
| 221 |
+
.asset-handle {
|
| 222 |
position: absolute;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
background: #fff;
|
| 224 |
+
border: 1px solid #000;
|
| 225 |
+
border-radius: 2px;
|
| 226 |
+
width: 10px;
|
| 227 |
+
height: 10px;
|
| 228 |
+
z-index: 10;
|
| 229 |
+
}
|
| 230 |
+
.asset-handle.resize {
|
| 231 |
+
bottom: -5px;
|
| 232 |
+
right: -5px;
|
| 233 |
+
cursor: nwse-resize;
|
| 234 |
+
}
|
| 235 |
+
.asset-handle.delete {
|
| 236 |
+
top: -5px;
|
| 237 |
+
right: -5px;
|
| 238 |
+
cursor: pointer;
|
| 239 |
+
background: #ef4444;
|
| 240 |
+
border-color: #b91c1c;
|
| 241 |
+
}
|
| 242 |
+
.asset-handle.delete::before {
|
| 243 |
+
content: 'Γ';
|
| 244 |
+
color: #fff;
|
| 245 |
+
font-size: 12px;
|
| 246 |
+
font-weight: bold;
|
| 247 |
+
position: absolute;
|
| 248 |
+
top: -2px;
|
| 249 |
+
left: 1px;
|
| 250 |
}
|
| 251 |
|
| 252 |
/* Loader */
|
| 253 |
.modern-loader{
|
| 254 |
display:none;position:absolute;inset:0;background:rgba(0,0,0,.92);
|
| 255 |
+
z-index:100;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(5px);
|
| 256 |
}
|
| 257 |
.modern-loader.active{display:flex}
|
| 258 |
.loader-spinner{
|
|
|
|
| 470 |
|
| 471 |
<!-- ββ Left icon rail ββ -->
|
| 472 |
<div class="rail">
|
| 473 |
+
<div class="rail-label">Tools</div>
|
| 474 |
<button class="rail-btn active" id="btn-brush" title="Brush">
|
| 475 |
<svg viewBox="0 0 24 24"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
|
| 476 |
</button>
|
|
|
|
| 480 |
<div class="color-input-wrap">
|
| 481 |
<input type="color" id="color-picker" class="color-input" value="#000000" title="Brush Color">
|
| 482 |
</div>
|
| 483 |
+
|
| 484 |
<div class="size-slider-wrap">
|
| 485 |
<input type="range" id="size-slider" class="size-slider" min="1" max="50" value="5" title="Brush Size">
|
| 486 |
</div>
|
| 487 |
|
| 488 |
<div class="rail-sep"></div>
|
| 489 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
<button class="rail-btn" id="btn-undo" title="Undo">
|
| 491 |
<svg viewBox="0 0 24 24"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
|
| 492 |
</button>
|
|
|
|
| 497 |
<svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
| 498 |
</button>
|
| 499 |
|
| 500 |
+
<div class="rail-sep"></div>
|
| 501 |
+
<div class="rail-label">Assets</div>
|
| 502 |
+
<button class="rail-btn" id="btn-asset" title="Add Asset / Sticker">
|
| 503 |
+
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
| 504 |
+
</button>
|
| 505 |
+
<input type="file" id="asset-file-input" accept="image/*" style="display:none;">
|
| 506 |
+
|
| 507 |
<div class="rail-sep"></div>
|
| 508 |
<div class="rail-label">Result</div>
|
| 509 |
<button class="rail-btn" id="btn-download" title="Download Result" disabled>
|
|
|
|
| 518 |
<button class="seg-btn active" id="seg-input">Sketch</button>
|
| 519 |
<button class="seg-btn" id="seg-result">Result</button>
|
| 520 |
</div>
|
| 521 |
+
<div class="canvas-meta" id="canvas-meta">Draw a sketch or add assets to begin</div>
|
| 522 |
<div class="canvas-actions"></div>
|
| 523 |
</div>
|
| 524 |
|
|
|
|
| 529 |
<div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
|
| 530 |
</div>
|
| 531 |
|
| 532 |
+
<div id="board-wrapper">
|
| 533 |
+
<canvas id="draw-canvas" width="1024" height="576" class="cursor-brush" style="aspect-ratio: 16 / 9;"></canvas>
|
| 534 |
+
<div id="asset-layer"></div>
|
| 535 |
+
<img id="result-img" alt="Result Image" style="display:none; aspect-ratio: 16 / 9;">
|
| 536 |
</div>
|
|
|
|
|
|
|
| 537 |
</div>
|
| 538 |
</div>
|
| 539 |
|
|
|
|
| 620 |
/* ββ DOM refs ββ */
|
| 621 |
const canvas = document.getElementById('draw-canvas');
|
| 622 |
const ctx = canvas.getContext('2d');
|
| 623 |
+
const assetLayer = document.getElementById('asset-layer');
|
| 624 |
const resultImg = document.getElementById('result-img');
|
| 625 |
const btnDownload = document.getElementById('btn-download');
|
| 626 |
|
|
|
|
| 631 |
const btnClear = document.getElementById('btn-clear');
|
| 632 |
const colorPicker = document.getElementById('color-picker');
|
| 633 |
const sizeSlider = document.getElementById('size-slider');
|
| 634 |
+
|
| 635 |
+
const btnAsset = document.getElementById('btn-asset');
|
| 636 |
+
const assetFileInput = document.getElementById('asset-file-input');
|
| 637 |
|
| 638 |
const promptInput = document.getElementById('custom-prompt-input');
|
| 639 |
const charCount = document.getElementById('char-count');
|
|
|
|
| 708 |
canvas.width = w;
|
| 709 |
canvas.height = h;
|
| 710 |
canvas.style.aspectRatio = ratio.replace(":", " / ");
|
| 711 |
+
|
| 712 |
resultImg.style.aspectRatio = ratio.replace(":", " / ");
|
| 713 |
|
| 714 |
+
setupCanvas(); // Clear and reset canvas on ratio change
|
| 715 |
+
undoStack = []; // Clear history on ratio change
|
| 716 |
redoStack = [];
|
| 717 |
+
saveState(); // Save initial blank state
|
| 718 |
|
| 719 |
+
// Clear assets on ratio change to prevent weird overlapping
|
| 720 |
+
assetLayer.innerHTML = "";
|
| 721 |
}
|
| 722 |
|
| 723 |
ratioSelect.addEventListener('change', (e) => {
|
|
|
|
| 744 |
}
|
| 745 |
|
| 746 |
function startDrawing(e) {
|
| 747 |
+
// Only draw if clicking directly on canvas, not an asset
|
| 748 |
+
if (e.target !== canvas) return;
|
| 749 |
e.preventDefault();
|
| 750 |
isDrawing = true;
|
| 751 |
const pos = getPos(e);
|
|
|
|
| 785 |
}
|
| 786 |
|
| 787 |
canvas.addEventListener('mousedown', startDrawing);
|
| 788 |
+
window.addEventListener('mousemove', draw); // Listen on window to allow dragging outside
|
| 789 |
+
window.addEventListener('mouseup', stopDrawing);
|
|
|
|
| 790 |
|
| 791 |
canvas.addEventListener('touchstart', startDrawing);
|
| 792 |
+
window.addEventListener('touchmove', draw, { passive: false });
|
| 793 |
+
window.addEventListener('touchend', stopDrawing);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
|
| 795 |
/* ββ Tool Listeners ββ */
|
| 796 |
btnBrush.addEventListener('click', () => {
|
|
|
|
| 825 |
btnClear.addEventListener('click', () => {
|
| 826 |
setupCanvas();
|
| 827 |
saveState();
|
| 828 |
+
assetLayer.innerHTML = ""; // Clear assets too
|
| 829 |
+
showToast('Canvas cleared', 'info');
|
| 830 |
});
|
| 831 |
|
| 832 |
btnUndo.addEventListener('click', async () => {
|
|
|
|
| 845 |
}
|
| 846 |
});
|
| 847 |
|
| 848 |
+
/* ββ Asset Management Logic ββ */
|
| 849 |
+
let activeAsset = null;
|
| 850 |
+
let activeResize = null;
|
| 851 |
+
let maxZ = 1;
|
| 852 |
+
|
| 853 |
+
btnAsset.addEventListener('click', () => {
|
| 854 |
+
assetFileInput.click();
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
assetFileInput.addEventListener('change', (e) => {
|
| 858 |
+
const file = e.target.files[0];
|
| 859 |
+
if (!file) return;
|
| 860 |
+
|
| 861 |
+
const reader = new FileReader();
|
| 862 |
+
reader.onload = (event) => {
|
| 863 |
+
addAsset(event.target.result);
|
| 864 |
+
};
|
| 865 |
+
reader.readAsDataURL(file);
|
| 866 |
+
e.target.value = ''; // Reset input
|
| 867 |
+
});
|
| 868 |
+
|
| 869 |
+
function addAsset(src) {
|
| 870 |
+
const wrapper = document.createElement('div');
|
| 871 |
+
wrapper.className = 'asset-wrapper';
|
| 872 |
+
|
| 873 |
+
const img = document.createElement('img');
|
| 874 |
+
img.className = 'asset-img';
|
| 875 |
+
img.src = src;
|
| 876 |
+
img.draggable = false;
|
| 877 |
+
|
| 878 |
+
const delBtn = document.createElement('div');
|
| 879 |
+
delBtn.className = 'asset-handle delete';
|
| 880 |
+
delBtn.addEventListener('mousedown', (e) => {
|
| 881 |
+
e.stopPropagation();
|
| 882 |
+
wrapper.remove();
|
| 883 |
+
});
|
| 884 |
+
delBtn.addEventListener('touchstart', (e) => {
|
| 885 |
+
e.stopPropagation();
|
| 886 |
+
wrapper.remove();
|
| 887 |
+
});
|
| 888 |
+
|
| 889 |
+
const resizeHandle = document.createElement('div');
|
| 890 |
+
resizeHandle.className = 'asset-handle resize';
|
| 891 |
+
|
| 892 |
+
wrapper.appendChild(img);
|
| 893 |
+
wrapper.appendChild(delBtn);
|
| 894 |
+
wrapper.appendChild(resizeHandle);
|
| 895 |
+
assetLayer.appendChild(wrapper);
|
| 896 |
+
|
| 897 |
+
// Set initial size & pos (center)
|
| 898 |
+
img.onload = () => {
|
| 899 |
+
const aspect = img.naturalWidth / img.naturalHeight;
|
| 900 |
+
let initW = 150;
|
| 901 |
+
let initH = initW / aspect;
|
| 902 |
+
wrapper.style.width = initW + 'px';
|
| 903 |
+
wrapper.style.height = initH + 'px';
|
| 904 |
+
|
| 905 |
+
const rect = canvas.getBoundingClientRect();
|
| 906 |
+
wrapper.style.left = (rect.width / 2 - initW / 2) + 'px';
|
| 907 |
+
wrapper.style.top = (rect.height / 2 - initH / 2) + 'px';
|
| 908 |
+
wrapper.style.zIndex = ++maxZ;
|
| 909 |
+
};
|
| 910 |
+
|
| 911 |
+
// Drag listeners
|
| 912 |
+
wrapper.addEventListener('mousedown', (e) => startDragAsset(e, wrapper));
|
| 913 |
+
wrapper.addEventListener('touchstart', (e) => startDragAsset(e, wrapper), { passive: false });
|
| 914 |
+
|
| 915 |
+
// Resize listeners
|
| 916 |
+
resizeHandle.addEventListener('mousedown', (e) => startResizeAsset(e, wrapper));
|
| 917 |
+
resizeHandle.addEventListener('touchstart', (e) => startResizeAsset(e, wrapper), { passive: false });
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
function startDragAsset(e, asset) {
|
| 921 |
+
e.preventDefault();
|
| 922 |
+
e.stopPropagation();
|
| 923 |
+
activeAsset = asset;
|
| 924 |
+
activeAsset.classList.add('active');
|
| 925 |
+
activeAsset.style.zIndex = ++maxZ;
|
| 926 |
+
|
| 927 |
+
const touch = e.touches ? e.touches[0] : e;
|
| 928 |
+
const rect = asset.getBoundingClientRect();
|
| 929 |
+
activeAsset._dragOffsetX = touch.clientX - rect.left;
|
| 930 |
+
activeAsset._dragOffsetY = touch.clientY - rect.top;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
function startResizeAsset(e, asset) {
|
| 934 |
+
e.preventDefault();
|
| 935 |
+
e.stopPropagation();
|
| 936 |
+
activeResize = asset;
|
| 937 |
+
activeResize.classList.add('active');
|
| 938 |
+
activeAsset.style.zIndex = ++maxZ;
|
| 939 |
+
|
| 940 |
+
const touch = e.touches ? e.touches[0] : e;
|
| 941 |
+
activeResize._startW = asset.offsetWidth;
|
| 942 |
+
activeResize._startH = asset.offsetHeight;
|
| 943 |
+
activeResize._startRX = touch.clientX;
|
| 944 |
+
activeResize._startRY = touch.clientY;
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
window.addEventListener('mousemove', (e) => {
|
| 948 |
+
if (activeAsset) {
|
| 949 |
+
const touch = e.touches ? e.touches[0] : e;
|
| 950 |
+
const wrapperRect = document.getElementById('board-wrapper').getBoundingClientRect();
|
| 951 |
+
let x = touch.clientX - wrapperRect.left - activeAsset._dragOffsetX;
|
| 952 |
+
let y = touch.clientY - wrapperRect.top - activeAsset._dragOffsetY;
|
| 953 |
+
|
| 954 |
+
// Clamp to wrapper bounds
|
| 955 |
+
x = Math.max(0, Math.min(x, wrapperRect.width - activeAsset.offsetWidth));
|
| 956 |
+
y = Math.max(0, Math.min(y, wrapperRect.height - activeAsset.offsetHeight));
|
| 957 |
+
|
| 958 |
+
activeAsset.style.left = x + 'px';
|
| 959 |
+
activeAsset.style.top = y + 'px';
|
| 960 |
+
}
|
| 961 |
+
if (activeResize) {
|
| 962 |
+
const touch = e.touches ? e.touches[0] : e;
|
| 963 |
+
const dx = touch.clientX - activeResize._startRX;
|
| 964 |
+
const aspect = activeResize._startH / activeResize._startW;
|
| 965 |
+
let newW = Math.max(20, activeResize._startW + dx);
|
| 966 |
+
let newH = newW * aspect;
|
| 967 |
+
activeResize.style.width = newW + 'px';
|
| 968 |
+
activeResize.style.height = newH + 'px';
|
| 969 |
+
}
|
| 970 |
+
});
|
| 971 |
+
|
| 972 |
+
window.addEventListener('touchmove', (e) => {
|
| 973 |
+
if (activeAsset || activeResize) {
|
| 974 |
+
e.preventDefault();
|
| 975 |
+
if (activeAsset) {
|
| 976 |
+
const touch = e.touches[0];
|
| 977 |
+
const wrapperRect = document.getElementById('board-wrapper').getBoundingClientRect();
|
| 978 |
+
let x = touch.clientX - wrapperRect.left - activeAsset._dragOffsetX;
|
| 979 |
+
let y = touch.clientY - wrapperRect.top - activeAsset._dragOffsetY;
|
| 980 |
+
x = Math.max(0, Math.min(x, wrapperRect.width - activeAsset.offsetWidth));
|
| 981 |
+
y = Math.max(0, Math.min(y, wrapperRect.height - activeAsset.offsetHeight));
|
| 982 |
+
activeAsset.style.left = x + 'px';
|
| 983 |
+
activeAsset.style.top = y + 'px';
|
| 984 |
+
}
|
| 985 |
+
if (activeResize) {
|
| 986 |
+
const touch = e.touches[0];
|
| 987 |
+
const dx = touch.clientX - activeResize._startRX;
|
| 988 |
+
const aspect = activeResize._startH / activeResize._startW;
|
| 989 |
+
let newW = Math.max(20, activeResize._startW + dx);
|
| 990 |
+
let newH = newW * aspect;
|
| 991 |
+
activeResize.style.width = newW + 'px';
|
| 992 |
+
activeResize.style.height = newH + 'px';
|
| 993 |
+
}
|
| 994 |
+
}
|
| 995 |
+
}, { passive: false });
|
| 996 |
+
|
| 997 |
+
function stopAssetAction() {
|
| 998 |
+
if (activeAsset) activeAsset.classList.remove('active');
|
| 999 |
+
if (activeResize) activeResize.classList.remove('active');
|
| 1000 |
+
activeAsset = null;
|
| 1001 |
+
activeResize = null;
|
| 1002 |
+
}
|
| 1003 |
+
window.addEventListener('mouseup', stopAssetAction);
|
| 1004 |
+
window.addEventListener('touchend', stopAssetAction);
|
| 1005 |
+
|
| 1006 |
+
/* ββ Compositing Logic ββ */
|
| 1007 |
+
function getCompositeImage() {
|
| 1008 |
+
const wrapper = document.getElementById('board-wrapper');
|
| 1009 |
+
const rect = wrapper.getBoundingClientRect();
|
| 1010 |
+
const scaleX = canvas.width / rect.width;
|
| 1011 |
+
const scaleY = canvas.height / rect.height;
|
| 1012 |
+
|
| 1013 |
+
const tempCanvas = document.createElement('canvas');
|
| 1014 |
+
tempCanvas.width = canvas.width;
|
| 1015 |
+
tempCanvas.height = canvas.height;
|
| 1016 |
+
const tempCtx = tempCanvas.getContext('2d');
|
| 1017 |
+
|
| 1018 |
+
// 1. Draw the base sketch
|
| 1019 |
+
tempCtx.drawImage(canvas, 0, 0);
|
| 1020 |
+
|
| 1021 |
+
// 2. Draw all assets on top
|
| 1022 |
+
const assets = assetLayer.querySelectorAll('.asset-wrapper');
|
| 1023 |
+
assets.forEach(asset => {
|
| 1024 |
+
const img = asset.querySelector('img');
|
| 1025 |
+
if (img.complete) {
|
| 1026 |
+
const x = asset.offsetLeft * scaleX;
|
| 1027 |
+
const y = asset.offsetTop * scaleY;
|
| 1028 |
+
const w = asset.offsetWidth * scaleX;
|
| 1029 |
+
const h = asset.offsetHeight * scaleY;
|
| 1030 |
+
tempCtx.drawImage(img, x, y, w, h);
|
| 1031 |
+
}
|
| 1032 |
+
});
|
| 1033 |
+
|
| 1034 |
+
return tempCanvas.toDataURL('image/png');
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
/* ββ Download Logic ββ */
|
| 1038 |
btnDownload.addEventListener('click', () => {
|
| 1039 |
if (resultImg.src && resultImg.src !== window.location.href) {
|
| 1040 |
const a = document.createElement('a');
|
|
|
|
| 1047 |
/* ββ UI Toggles ββ */
|
| 1048 |
function setViewMode(mode) {
|
| 1049 |
if (mode === 'result') {
|
| 1050 |
+
canvas.style.display = 'none';
|
| 1051 |
+
assetLayer.style.display = 'none';
|
| 1052 |
resultImg.style.display = 'block';
|
| 1053 |
segResult.classList.add('active');
|
| 1054 |
segInput.classList.remove('active');
|
| 1055 |
canvasMeta.textContent = 'Generated Result';
|
| 1056 |
} else {
|
| 1057 |
+
canvas.style.display = 'block';
|
| 1058 |
+
assetLayer.style.display = 'block';
|
| 1059 |
resultImg.style.display = 'none';
|
| 1060 |
segResult.classList.remove('active');
|
| 1061 |
segInput.classList.add('active');
|
| 1062 |
+
canvasMeta.textContent = 'Draw a sketch or add assets to begin';
|
| 1063 |
}
|
| 1064 |
}
|
| 1065 |
segInput.addEventListener('click', () => setViewMode('input'));
|
|
|
|
| 1181 |
cancelBtn.style.display = on ? 'flex' : 'none';
|
| 1182 |
}
|
| 1183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
async function runEdit() {
|
| 1185 |
if (running) return;
|
| 1186 |
|
|
|
|
| 1192 |
showLoader('Submitting to queueβ¦');
|
| 1193 |
btnDownload.disabled = true;
|
| 1194 |
|
| 1195 |
+
// Composite sketch and assets into a single image
|
| 1196 |
+
const sketch_b64 = getCompositeImage();
|
| 1197 |
const selectedRatio = ratioSelect.value;
|
| 1198 |
const [targetW, targetH] = dims[selectedRatio];
|
| 1199 |
|
|
|
|
| 1245 |
}
|
| 1246 |
hideLoader('Done', 'done');
|
| 1247 |
showToast('Image generated successfully', 'info');
|
| 1248 |
+
btnDownload.disabled = false; // Enable download
|
| 1249 |
} else {
|
| 1250 |
hideLoader('Done', 'done');
|
| 1251 |
}
|