LTM / index.html
mlmihjaz's picture
Update index.html
dbd3573 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DesignForge Pro</title>
<script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#f5f5f5;--surf:#ffffff;--surf2:#f8f8f8;--surf3:#efefef;
--brd:#e5e5e5;--brd2:#d0d0d0;
--acc:#7c3aed;--acc-lt:#8b5cf6;--acc-dark:#6d28d9;--acc-bg:#ede9fe;
--txt:#111111;--txt2:#444444;--txt3:#888888;--txt4:#bbbbbb;
--grn:#10b981;--red:#ef4444;--red-bg:#fee2e2;--amb:#f59e0b;
--blue:#3b82f6;--blue-bg:#dbeafe;
--r:6px;--rm:10px;--rl:14px;
--sans:'DM Sans',sans-serif;--mono:'DM Mono',monospace;
--sh-sm:0 1px 3px rgba(0,0,0,.08);--sh:0 4px 12px rgba(0,0,0,.1);--sh-lg:0 16px 40px rgba(0,0,0,.14)
}
body{font-family:var(--sans);background:var(--bg);color:var(--txt);height:100vh;display:flex;flex-direction:column;overflow:hidden;font-size:13px;line-height:1.4}
/* TOPBAR */
#topbar{display:flex;align-items:center;height:52px;background:var(--surf);border-bottom:1px solid var(--brd);flex-shrink:0;padding:0 12px;box-shadow:var(--sh-sm);z-index:100;gap:4px}
#logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:17px;color:var(--txt);text-decoration:none;padding:0 14px 0 4px;white-space:nowrap;border-right:1px solid var(--brd);margin-right:12px;height:100%}
#logo .brand-dot{color:var(--acc)}
.top-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:var(--rm);font-size:13px;font-weight:500;cursor:pointer;border:none;background:transparent;color:var(--txt2);transition:all .12s;white-space:nowrap;font-family:var(--sans)}
.top-btn:hover{background:var(--surf3);color:var(--txt)}
.top-btn.pri{background:var(--acc);color:#fff;padding:7px 18px}
.top-btn.pri:hover{background:var(--acc-dark)}
.top-btn.outline{border:1.5px solid var(--brd2);color:var(--txt)}
.top-btn.outline:hover{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.top-sep{width:1px;height:24px;background:var(--brd);margin:0 6px;flex-shrink:0}
#doc-name{font-size:14px;font-weight:600;color:var(--txt);border:1.5px solid transparent;border-radius:var(--r);padding:4px 8px;outline:none;background:transparent;width:180px;transition:border-color .12s}
#doc-name:hover{border-color:var(--brd2)}
#doc-name:focus{border-color:var(--acc);background:var(--surf)}
.top-right{margin-left:auto;display:flex;align-items:center;gap:6px}
.zoom-ctrl{display:flex;align-items:center;gap:2px;background:var(--surf3);border:1px solid var(--brd);border-radius:var(--rm);padding:2px}
.zoom-ctrl button{width:26px;height:26px;border:none;background:transparent;border-radius:7px;cursor:pointer;font-size:15px;color:var(--txt2);display:flex;align-items:center;justify-content:center;transition:background .1s}
.zoom-ctrl button:hover{background:var(--surf)}
#zoom-pct{font-size:12px;font-weight:600;color:var(--txt);min-width:44px;text-align:center;cursor:pointer;border:none;background:transparent;outline:none;font-family:var(--sans)}
/* TOAST */
#toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(12px);background:#18181b;color:#fff;border-radius:var(--rm);padding:9px 18px;font-size:13px;font-weight:500;opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:9999;box-shadow:var(--sh-lg);white-space:nowrap}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.err{background:var(--red)}
/* WORKSPACE */
#workspace{display:flex;flex:1;overflow:hidden}
/* LEFT TABS */
#left-tabs{width:52px;background:var(--surf);border-right:1px solid var(--brd);display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:2px;flex-shrink:0}
.tab-icon{width:40px;height:40px;border-radius:var(--rm);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;cursor:pointer;color:var(--txt3);transition:all .12s;position:relative}
.tab-icon:hover{background:var(--surf3);color:var(--txt)}
.tab-icon.active{background:var(--acc-bg);color:var(--acc)}
.tab-icon .tab-label{font-size:9px;font-weight:600;line-height:1;text-transform:uppercase;letter-spacing:.04em}
.tab-icon svg{width:18px;height:18px}
/* LEFT PANEL */
#left-panel{width:0;overflow:hidden;background:var(--surf);border-right:1px solid var(--brd);transition:width .2s ease;flex-shrink:0;display:flex;flex-direction:column}
#left-panel.open{width:268px}
.panel-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:1px solid var(--brd);flex-shrink:0}
.panel-title{font-size:14px;font-weight:700;color:var(--txt)}
.panel-close{width:26px;height:26px;border:none;background:transparent;cursor:pointer;color:var(--txt3);border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .1s}
.panel-close:hover{background:var(--surf3);color:var(--txt)}
.panel-body{flex:1;overflow-y:auto;padding:12px}
.panel-body::-webkit-scrollbar{width:4px}
.panel-body::-webkit-scrollbar-thumb{background:var(--brd2);border-radius:2px}
.panel-section{display:none}
.panel-section.active{display:block}
/* Upload */
.upload-zone{border:2px dashed var(--brd2);border-radius:var(--rl);padding:24px 16px;text-align:center;cursor:pointer;transition:all .15s;background:var(--surf2);margin-bottom:14px}
.upload-zone:hover,.upload-zone.drag-over{border-color:var(--acc);background:var(--acc-bg)}
.upload-zone .uz-icon{font-size:28px;margin-bottom:6px;opacity:.7}
.upload-zone .uz-title{font-size:13px;font-weight:600;color:var(--txt);margin-bottom:3px}
.upload-zone .uz-sub{font-size:11px;color:var(--txt3)}
.upload-zone .uz-btn{display:inline-block;margin-top:8px;padding:5px 14px;background:var(--acc);color:#fff;border-radius:var(--rm);font-size:12px;font-weight:600}
.url-input-row{display:flex;gap:6px;margin-bottom:14px}
.url-field{flex:1;padding:7px 10px;border:1.5px solid var(--brd);border-radius:var(--r);font-size:12px;font-family:var(--sans);outline:none;color:var(--txt);background:var(--surf);transition:border-color .12s}
.url-field:focus{border-color:var(--acc)}
.url-field::placeholder{color:var(--txt4)}
.p-btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:7px 12px;border-radius:var(--r);font-size:12px;font-weight:600;cursor:pointer;border:1.5px solid var(--brd);background:var(--surf);color:var(--txt);transition:all .1s;white-space:nowrap;font-family:var(--sans)}
.p-btn:hover{border-color:var(--brd2);background:var(--surf2)}
.p-btn:active{transform:scale(0.97)}
.p-btn.acc{background:var(--acc);border-color:var(--acc);color:#fff}
.p-btn.acc:hover{background:var(--acc-dark);border-color:var(--acc-dark)}
.p-btn.full{width:100%}
.p-btn.danger{border-color:var(--red);color:var(--red)}
.p-btn.danger:hover{background:var(--red-bg)}
.sec-label{font-size:11px;font-weight:700;color:var(--txt3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;margin-top:4px;display:flex;align-items:center;gap:6px}
.thumb-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:12px}
.thumb-item{aspect-ratio:4/3;border-radius:var(--r);overflow:hidden;cursor:pointer;position:relative;background:var(--surf3);border:1.5px solid transparent;transition:all .12s}
.thumb-item:hover{border-color:var(--acc);transform:scale(1.02);box-shadow:var(--sh)}
.thumb-item img{width:100%;height:100%;object-fit:cover;display:block}
.thumb-item .thumb-label{position:absolute;bottom:0;left:0;right:0;padding:4px 6px;background:linear-gradient(transparent,rgba(0,0,0,.55));color:#fff;font-size:9px;font-weight:600}
/* SHAPES — category grid */
.shapes-search{width:100%;padding:7px 10px;border:1.5px solid var(--brd);border-radius:var(--rm);font-size:12px;outline:none;color:var(--txt);background:var(--surf2);margin-bottom:10px;transition:all .12s;font-family:var(--sans)}
.shapes-search:focus{border-color:var(--acc);background:var(--surf)}
.shapes-search::placeholder{color:var(--txt4)}
.shape-category{margin-bottom:10px}
.shape-cat-label{font-size:10px;font-weight:700;color:var(--txt3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px;padding:2px 0}
.shape-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
.shape-btn{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:var(--surf2);border:1.5px solid var(--brd);border-radius:var(--r);cursor:pointer;transition:all .1s;padding:4px;position:relative;overflow:hidden}
.shape-btn:hover{border-color:var(--acc);background:var(--acc-bg);transform:scale(1.06)}
.shape-btn svg{width:24px;height:24px;display:block}
.shape-btn .s-label{font-size:8px;color:var(--txt3);text-align:center;line-height:1;white-space:nowrap;overflow:hidden;max-width:100%}
.shape-btn:hover .s-label{color:var(--acc)}
/* Shape style controls */
.shape-style-row{display:flex;align-items:center;gap:6px;margin-bottom:7px}
.shape-style-row .ss-label{font-size:11px;color:var(--txt3);min-width:50px;flex-shrink:0}
.shape-no-fill-row{display:flex;align-items:center;gap:6px;margin-bottom:7px;font-size:11px;color:var(--txt2)}
input[type="color"]{width:28px;height:28px;border:1.5px solid var(--brd);border-radius:6px;cursor:pointer;padding:2px;background:var(--surf)}
.num-field{width:58px;padding:5px 7px;border:1.5px solid var(--brd);border-radius:var(--r);font-size:12px;font-family:var(--mono);outline:none;color:var(--txt);background:var(--surf);text-align:center;transition:border-color .12s}
.num-field:focus{border-color:var(--acc)}
/* Text presets */
.text-preset{display:block;width:100%;padding:10px 12px;background:var(--surf2);border:1.5px solid var(--brd);border-radius:var(--r);cursor:pointer;margin-bottom:7px;transition:all .1s;text-align:left;font-family:var(--sans);color:var(--txt)}
.text-preset:hover{border-color:var(--acc);background:var(--acc-bg)}
.text-preset.heading{font-size:18px;font-weight:700}
.text-preset.subheading{font-size:14px;font-weight:600}
.text-preset.body-text{font-size:12px}
.text-preset.caption{font-size:10px;color:var(--txt3)}
.icon-row{display:flex;gap:4px;margin-bottom:8px}
.ir-btn{flex:1;height:30px;border:1.5px solid var(--brd);border-radius:var(--r);background:var(--surf);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;color:var(--txt2);transition:all .1s;font-family:var(--sans)}
.ir-btn:hover{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.ir-btn.active{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.chk-row{display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:pointer;font-size:12px;color:var(--txt2)}
.chk-row input[type="checkbox"]{accent-color:var(--acc);width:14px;height:14px;cursor:pointer}
.input-field{width:100%;padding:7px 10px;border:1.5px solid var(--brd);border-radius:var(--r);font-size:12px;font-family:var(--sans);outline:none;color:var(--txt);background:var(--surf);transition:border-color .12s}
.input-field:focus{border-color:var(--acc)}
.input-field::placeholder{color:var(--txt4)}
.fc-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.fc-label{font-size:11px;color:var(--txt3);min-width:56px;flex-shrink:0}
.fc-val{font-size:11px;font-weight:600;color:var(--txt);font-family:var(--mono);min-width:36px;text-align:right}
input[type="range"]{flex:1;height:4px;border-radius:2px;background:var(--surf3);outline:none;border:none;cursor:pointer;-webkit-appearance:none;accent-color:var(--acc)}
input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--acc);cursor:pointer;box-shadow:0 1px 4px rgba(124,58,237,.4)}
select{width:100%;padding:7px 10px;border:1.5px solid var(--brd);border-radius:var(--r);font-size:12px;font-family:var(--sans);outline:none;color:var(--txt);background:var(--surf);cursor:pointer;transition:border-color .12s;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
select:focus{border-color:var(--acc)}
.bg-colors{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px}
.bg-chip{width:22px;height:22px;border-radius:5px;border:1.5px solid rgba(0,0,0,.08);cursor:pointer;transition:transform .1s}
.bg-chip:hover{transform:scale(1.18)}
.sub-section{margin-bottom:14px}
/* CANVAS */
#canvas-wrap{flex:1;display:flex;align-items:center;justify-content:center;background:#e8e8e8;background-image:linear-gradient(45deg,#d8d8d8 25%,transparent 25%),linear-gradient(-45deg,#d8d8d8 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#d8d8d8 75%),linear-gradient(-45deg,transparent 75%,#d8d8d8 75%);background-size:20px 20px;background-position:0 0,0 10px,10px -10px,-10px 0;overflow:hidden;position:relative}
#canvas-wrap.drag-over{outline:3px solid var(--acc);outline-offset:-3px}
#canvas-container{position:relative;box-shadow:0 8px 40px rgba(0,0,0,.25),0 2px 8px rgba(0,0,0,.1);border-radius:2px;transform-origin:center center}
#canvas-toolbar{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:4px;background:var(--surf);border:1px solid var(--brd);border-radius:30px;padding:5px 8px;box-shadow:var(--sh);z-index:20;opacity:0;pointer-events:none;transition:opacity .15s}
#canvas-toolbar.visible{opacity:1;pointer-events:all}
.ct-btn{width:30px;height:30px;border-radius:50%;border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;color:var(--txt2);transition:all .1s}
.ct-btn:hover{background:var(--surf3);color:var(--txt)}
.ct-sep{width:1px;height:20px;background:var(--brd);margin:0 2px}
#onboard{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;pointer-events:none;z-index:4}
#onboard .ob-icon{font-size:64px;opacity:.12}
#onboard .ob-title{font-size:18px;font-weight:700;color:var(--txt3);opacity:.6}
#onboard .ob-actions{display:flex;gap:10px;pointer-events:all;opacity:.7}
/* RIGHT PANEL */
#right-panel{width:240px;min-width:240px;background:var(--surf);border-left:1px solid var(--brd);display:flex;flex-direction:column;overflow-y:auto;flex-shrink:0}
#right-panel::-webkit-scrollbar{width:4px}
#right-panel::-webkit-scrollbar-thumb{background:var(--brd2);border-radius:2px}
.rp-section{padding:14px;border-bottom:1px solid var(--brd)}
.rp-title{font-size:11px;font-weight:700;color:var(--txt3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.dim-group{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
.dim-label{font-size:10px;color:var(--txt3);margin-bottom:3px}
.dim-input{width:100%;padding:6px 8px;border:1.5px solid var(--brd);border-radius:var(--r);font-size:12px;font-family:var(--mono);outline:none;color:var(--txt);background:var(--surf);text-align:center;transition:border-color .12s}
.dim-input:focus{border-color:var(--acc)}
.pos-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
.xform-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
.xf-btn{height:28px;border:1.5px solid var(--brd);border-radius:var(--r);background:var(--surf);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;color:var(--txt2);transition:all .1s}
.xf-btn:hover{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.fmt-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px}
.fmt-card{padding:10px 6px;border:2px solid var(--brd);border-radius:var(--rm);text-align:center;cursor:pointer;transition:all .1s}
.fmt-card:hover{border-color:var(--brd2)}
.fmt-card.sel{border-color:var(--acc);background:var(--acc-bg)}
.fmt-card .fmt-name{font-size:13px;font-weight:700;color:var(--txt)}
.fmt-card .fmt-sub{font-size:10px;color:var(--txt3);margin-top:2px}
.effect-list{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px}
.ef-chip{padding:4px 10px;border:1.5px solid var(--brd);border-radius:20px;font-size:11px;font-weight:500;cursor:pointer;color:var(--txt2);transition:all .1s}
.ef-chip:hover{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.ef-chip.active{border-color:var(--acc);color:var(--acc);background:var(--acc-bg)}
.layer-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--r);cursor:pointer;transition:all .1s;margin-bottom:2px;user-select:none}
.layer-item:hover{background:var(--surf2)}
.layer-item.active{background:var(--acc-bg)}
.layer-icon{width:22px;height:22px;background:var(--surf3);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0}
.layer-item.active .layer-icon{background:var(--acc);color:#fff}
.layer-name{flex:1;font-size:12px;font-weight:500;color:var(--txt);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px}
.layer-vis{width:20px;height:20px;border:none;background:transparent;cursor:pointer;color:var(--txt3);font-size:12px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:0;transition:opacity .1s}
.layer-item:hover .layer-vis,.layer-item.active .layer-vis{opacity:1}
/* CROP */
#crop-overlay{position:absolute;inset:0;z-index:30;display:none}
#crop-overlay.active{display:block}
#crop-box{position:absolute;border:2px solid #fff;cursor:move}
.crop-handle{position:absolute;width:10px;height:10px;background:#fff;border:1.5px solid var(--acc);border-radius:2px}
.ch-tl{top:-5px;left:-5px;cursor:nw-resize}.ch-tr{top:-5px;right:-5px;cursor:ne-resize}
.ch-bl{bottom:-5px;left:-5px;cursor:sw-resize}.ch-br{bottom:-5px;right:-5px;cursor:se-resize}
.ch-tm{top:-5px;left:50%;transform:translateX(-50%);cursor:n-resize}
.ch-bm{bottom:-5px;left:50%;transform:translateX(-50%);cursor:s-resize}
.ch-ml{left:-5px;top:50%;transform:translateY(-50%);cursor:w-resize}
.ch-mr{right:-5px;top:50%;transform:translateY(-50%);cursor:e-resize}
#crop-toolbar{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;background:var(--surf);border:1px solid var(--brd);border-radius:30px;padding:6px 10px;box-shadow:var(--sh-lg)}
/* MODALS */
#modal-overlay,#size-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(4px);z-index:500;display:none;align-items:center;justify-content:center}
#modal-overlay.open,#size-modal-overlay.open{display:flex}
#export-modal,#size-modal{background:var(--surf);border-radius:var(--rl);width:460px;box-shadow:var(--sh-lg);overflow:hidden;animation:modalIn .2s ease}
#size-modal{width:500px}
@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(8px)}to{opacity:1;transform:none}}
.modal-header{display:flex;align-items:center;justify-content:space-between;padding:18px 20px 14px;border-bottom:1px solid var(--brd)}
.modal-title{font-size:16px;font-weight:700;color:var(--txt)}
.modal-close{width:28px;height:28px;border:none;background:transparent;border-radius:var(--r);cursor:pointer;font-size:18px;color:var(--txt3);display:flex;align-items:center;justify-content:center}
.modal-close:hover{background:var(--surf3);color:var(--txt)}
.modal-body{padding:18px 20px}
.modal-footer{padding:14px 20px 18px;border-top:1px solid var(--brd);display:flex;align-items:center;justify-content:flex-end;gap:8px}
.size-presets{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
.size-preset{padding:10px 8px;border:1.5px solid var(--brd);border-radius:var(--rm);cursor:pointer;transition:all .1s;text-align:center}
.size-preset:hover{border-color:var(--acc);background:var(--acc-bg)}
.size-preset.sel{border-color:var(--acc);background:var(--acc-bg)}
.size-preset .sp-name{font-size:12px;font-weight:600;color:var(--txt)}
.size-preset .sp-dim{font-size:10px;color:var(--txt3);margin-top:2px;font-family:var(--mono)}
/* CTX MENU */
#ctx-menu{position:fixed;background:var(--surf);border:1.5px solid var(--brd);border-radius:var(--rm);box-shadow:var(--sh-lg);padding:6px;z-index:1000;min-width:160px;display:none;animation:fadeIn .1s ease}
@keyframes fadeIn{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:none}}
.ctx-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--r);cursor:pointer;font-size:12px;color:var(--txt2);transition:all .08s}
.ctx-item:hover{background:var(--acc-bg);color:var(--acc)}
.ctx-item.danger:hover{background:var(--red-bg);color:var(--red)}
.ctx-sep{height:1px;background:var(--brd);margin:4px 0}
.ctx-icon{width:16px;text-align:center;flex-shrink:0}
/* STATUS BAR */
#statusbar{display:flex;align-items:center;gap:14px;padding:5px 14px;background:var(--surf);border-top:1px solid var(--brd);font-size:11px;font-family:var(--mono);color:var(--txt3);flex-shrink:0}
.sb-dot{width:6px;height:6px;border-radius:50%;background:var(--grn);flex-shrink:0}
.sb-dot.warn{background:var(--amb)}
.sb-item{display:flex;align-items:center;gap:5px}
.sb-sep{width:1px;height:14px;background:var(--brd2)}
input[type="file"]{display:none}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--brd2);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--txt4)}
.badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:20px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}
.badge.purple{background:var(--acc-bg);color:var(--acc)}
.badge.green{background:#d1fae5;color:var(--grn)}
.toggle-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px}
.toggle-label{font-size:12px;color:var(--txt2)}
.toggle{width:34px;height:18px;background:var(--brd2);border-radius:9px;cursor:pointer;position:relative;transition:background .15s;border:none;flex-shrink:0}
.toggle.on{background:var(--acc)}
.toggle::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:left .15s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
.toggle.on::after{left:18px}
.grad-presets-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:12px}
.grad-item{height:36px;border-radius:var(--r);cursor:pointer;border:1.5px solid transparent;transition:all .12s}
.grad-item:hover{border-color:var(--acc);transform:scale(1.02)}
</style>
</head>
<body>
<div id="topbar">
<a id="logo" href="#">
<svg width="26" height="26" viewBox="0 0 26 26" fill="none"><rect width="26" height="26" rx="7" fill="#7c3aed"/><path d="M7 19L13 7l6 12" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 15h8" stroke="white" stroke-width="2.5" stroke-linecap="round"/></svg>
Design<span class="brand-dot">Forge</span>
</a>
<input type="text" id="doc-name" value="Untitled Design">
<div class="top-sep"></div>
<button class="top-btn" id="btn-undo">↩ Undo</button>
<button class="top-btn" id="btn-redo">↪ Redo</button>
<div class="top-sep"></div>
<button class="top-btn" id="btn-canvas-size">⊡ Resize</button>
<div class="top-right">
<div class="zoom-ctrl">
<button id="z-out"></button>
<input type="text" id="zoom-pct" value="100%" readonly>
<button id="z-in">+</button>
<button id="z-fit" style="font-size:11px"></button>
</div>
<div class="top-sep"></div>
<button class="top-btn outline" id="btn-export-open">⬇ Download</button>
<button class="top-btn pri" id="btn-share">Share</button>
</div>
</div>
<div id="toast"></div>
<div id="workspace">
<!-- ICON TABS -->
<div id="left-tabs">
<div class="tab-icon active" data-panel="upload">
<svg viewBox="0 0 20 20" fill="none"><path d="M10 13V3M6 7l4-4 4 4M4 16h12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="tab-label">Upload</span>
</div>
<div class="tab-icon" data-panel="templates">
<svg viewBox="0 0 20 20" fill="none"><rect x="2" y="2" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="11" y="2" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="2" y="11" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="11" y="11" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>
<span class="tab-label">Templ</span>
</div>
<div class="tab-icon" data-panel="elements">
<svg viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5"/><path d="M7 7h6v6H7z" stroke="currentColor" stroke-width="1.5"/></svg>
<span class="tab-label">Shapes</span>
</div>
<div class="tab-icon" data-panel="text">
<svg viewBox="0 0 20 20" fill="none"><path d="M4 5h12M10 5v10M7 15h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span class="tab-label">Text</span>
</div>
<div class="tab-icon" data-panel="background">
<svg viewBox="0 0 20 20" fill="none"><path d="M3 14l4-4 4 4 4-6 3 6H3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="6" cy="7" r="2" stroke="currentColor" stroke-width="1.5"/></svg>
<span class="tab-label">BG</span>
</div>
<div style="flex:1"></div>
<div class="tab-icon" data-panel="layers">
<svg viewBox="0 0 20 20" fill="none"><path d="M3 7l7-4 7 4-7 4-7-4zM3 13l7 4 7-4M3 10l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
<span class="tab-label">Layers</span>
</div>
</div>
<!-- LEFT PANEL -->
<div id="left-panel" class="open">
<div class="panel-header">
<span class="panel-title" id="panel-title">Upload</span>
<button class="panel-close" id="panel-close-btn">×</button>
</div>
<div class="panel-body" id="panel-body">
<!-- UPLOAD -->
<div class="panel-section active" id="sec-upload">
<div class="upload-zone" id="upload-zone">
<div class="uz-icon"></div>
<div class="uz-title">Drag files here</div>
<div class="uz-sub">or click to browse</div>
<span class="uz-btn">Browse files</span>
</div>
<input type="file" id="file-input" accept="image/*,image/svg+xml">
<div class="sec-label">Load from URL</div>
<div class="url-input-row">
<input type="text" class="url-field" id="url-input" placeholder="https://…/image.png">
<button class="p-btn acc" id="btn-load-url">Go</button>
</div>
<div class="sec-label">Samples</div>
<div class="thumb-grid" id="sample-grid">
<div class="thumb-item" data-url="https://picsum.photos/seed/abc/800/600"><img src="https://picsum.photos/seed/abc/120/90" alt="" loading="lazy"><span class="thumb-label">Nature</span></div>
<div class="thumb-item" data-url="https://picsum.photos/seed/xyz/800/600"><img src="https://picsum.photos/seed/xyz/120/90" alt="" loading="lazy"><span class="thumb-label">Landscape</span></div>
<div class="thumb-item" data-url="https://picsum.photos/seed/city/800/600"><img src="https://picsum.photos/seed/city/120/90" alt="" loading="lazy"><span class="thumb-label">City</span></div>
<div class="thumb-item" data-url="https://picsum.photos/seed/tech/800/600"><img src="https://picsum.photos/seed/tech/120/90" alt="" loading="lazy"><span class="thumb-label">Abstract</span></div>
<div class="thumb-item" data-url="https://picsum.photos/seed/food/800/500"><img src="https://picsum.photos/seed/food/120/90" alt="" loading="lazy"><span class="thumb-label">Portrait</span></div>
<div class="thumb-item" data-url="https://picsum.photos/seed/ocean/800/600"><img src="https://picsum.photos/seed/ocean/120/90" alt="" loading="lazy"><span class="thumb-label">Ocean</span></div>
</div>
<div class="sec-label">Clipboard</div>
<button class="p-btn full" id="btn-paste">⎘ Paste from clipboard</button>
</div>
<!-- TEMPLATES -->
<div class="panel-section" id="sec-templates">
<div class="sec-label">Social Media <span class="badge purple">Popular</span></div>
<div class="thumb-grid">
<div class="thumb-item template-item" data-w="1080" data-h="1080" style="background:linear-gradient(135deg,#667eea,#764ba2);aspect-ratio:1"><span class="thumb-label">Instagram Post</span></div>
<div class="thumb-item template-item" data-w="1920" data-h="1080" style="background:linear-gradient(135deg,#f093fb,#f5576c)"><span class="thumb-label">YouTube Cover</span></div>
<div class="thumb-item template-item" data-w="1500" data-h="500" style="background:linear-gradient(135deg,#4facfe,#00f2fe);aspect-ratio:3/1"><span class="thumb-label">Twitter Header</span></div>
<div class="thumb-item template-item" data-w="1200" data-h="628" style="background:linear-gradient(135deg,#43e97b,#38f9d7)"><span class="thumb-label">Facebook Post</span></div>
</div>
<div class="sec-label">Marketing</div>
<div class="thumb-grid">
<div class="thumb-item template-item" data-w="1200" data-h="630" style="background:linear-gradient(135deg,#fa709a,#fee140)"><span class="thumb-label">OG Image</span></div>
<div class="thumb-item template-item" data-w="800" data-h="450" style="background:linear-gradient(135deg,#a18cd1,#fbc2eb)"><span class="thumb-label">Blog Header</span></div>
<div class="thumb-item template-item" data-w="300" data-h="250" style="background:linear-gradient(135deg,#0fd850,#f9f047)"><span class="thumb-label">Banner Ad</span></div>
<div class="thumb-item template-item" data-w="728" data-h="90" style="background:linear-gradient(90deg,#89f7fe,#66a6ff);aspect-ratio:8/1"><span class="thumb-label">Leaderboard</span></div>
</div>
<div class="sec-label">Print</div>
<div class="thumb-grid">
<div class="thumb-item template-item" data-w="2480" data-h="3508" style="background:linear-gradient(135deg,#ffecd2,#fcb69f);aspect-ratio:2/3"><span class="thumb-label">A4 Portrait</span></div>
<div class="thumb-item template-item" data-w="3508" data-h="2480" style="background:linear-gradient(135deg,#a1c4fd,#c2e9fb)"><span class="thumb-label">A4 Landscape</span></div>
</div>
</div>
<!-- ELEMENTS / SHAPES -->
<div class="panel-section" id="sec-elements">
<!-- Style controls at top -->
<div style="background:var(--surf2);border:1px solid var(--brd);border-radius:var(--rm);padding:10px;margin-bottom:10px">
<div class="shape-style-row">
<span class="ss-label">Fill</span>
<input type="color" id="sh-fill" value="#7c3aed">
<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--txt2);margin-left:4px">
<input type="checkbox" id="sh-no-fill" style="accent-color:var(--acc);width:13px;height:13px"> None
</label>
</div>
<div class="shape-style-row">
<span class="ss-label">Stroke</span>
<input type="color" id="sh-stroke" value="#111111">
<input type="number" class="num-field" id="sh-stroke-w" value="0" min="0" max="30" style="width:52px;margin-left:4px">
</div>
<div class="shape-style-row">
<span class="ss-label">Opacity</span>
<input type="range" id="sh-opacity" min="0" max="1" step="0.01" value="1" style="flex:1">
<span id="sh-op-val" style="font-size:11px;font-family:var(--mono);min-width:32px;text-align:right">100%</span>
</div>
</div>
<input type="text" class="shapes-search" id="shape-search" placeholder="Search shapes…">
<div id="shapes-container">
<!-- filled by JS -->
</div>
</div>
<!-- TEXT -->
<div class="panel-section" id="sec-text">
<div class="sec-label">Add Text</div>
<button class="text-preset heading" id="tp-heading">Add a heading</button>
<button class="text-preset subheading" id="tp-sub">Add a subheading</button>
<button class="text-preset body-text" id="tp-body">Add body text</button>
<button class="text-preset caption" id="tp-caption">Add caption</button>
<div class="sec-label" style="margin-top:10px">Font</div>
<select id="txt-font" style="margin-bottom:8px">
<optgroup label="System"><option value="Arial">Arial</option><option value="Georgia">Georgia</option><option value="Times New Roman">Times New Roman</option><option value="Verdana">Verdana</option><option value="Impact">Impact</option></optgroup>
<optgroup label="Sans-Serif"><option value="'Poppins',sans-serif">Poppins</option><option value="'Montserrat',sans-serif">Montserrat</option><option value="'Raleway',sans-serif">Raleway</option><option value="'Nunito',sans-serif">Nunito</option><option value="'Inter',sans-serif">Inter</option><option value="'Space Grotesk',sans-serif">Space Grotesk</option><option value="'Plus Jakarta Sans',sans-serif">Plus Jakarta Sans</option><option value="'Manrope',sans-serif">Manrope</option><option value="'Sora',sans-serif">Sora</option></optgroup>
<optgroup label="Serif"><option value="'Playfair Display',serif">Playfair Display</option><option value="'Lora',serif">Lora</option><option value="'Merriweather',serif">Merriweather</option><option value="'Cormorant',serif">Cormorant</option><option value="'Abril Fatface',serif">Abril Fatface</option></optgroup>
<optgroup label="Display"><option value="'Bebas Neue',sans-serif">Bebas Neue</option><option value="'Pacifico',cursive">Pacifico</option><option value="'Dancing Script',cursive">Dancing Script</option><option value="'Caveat',cursive">Caveat</option><option value="'Russo One',sans-serif">Russo One</option></optgroup>
<optgroup label="Mono"><option value="'JetBrains Mono',monospace">JetBrains Mono</option><option value="'Fira Code',monospace">Fira Code</option><option value="'Space Mono',monospace">Space Mono</option></optgroup>
</select>
<div class="fc-row">
<span class="fc-label">Size</span>
<input type="number" id="txt-size" value="36" min="6" max="400" class="num-field" style="width:62px">
<span class="fc-label">Color</span>
<input type="color" id="txt-color" value="#111111">
</div>
<div class="icon-row" style="margin-bottom:8px">
<button class="ir-btn" id="txt-bold" style="font-weight:700">B</button>
<button class="ir-btn" id="txt-italic" style="font-style:italic">I</button>
<button class="ir-btn" id="txt-underline" style="text-decoration:underline">U</button>
<button class="ir-btn" id="txt-align-l"></button>
<button class="ir-btn" id="txt-align-c"></button>
<button class="ir-btn" id="txt-align-r"></button>
</div>
<div class="fc-row">
<span class="fc-label">Opacity</span>
<input type="range" id="txt-opacity" min="0" max="1" step="0.01" value="1">
<span class="fc-val" id="txt-op-val">100%</span>
</div>
<div class="fc-row">
<span class="fc-label">Stroke</span>
<input type="color" id="txt-stroke-color" value="#000000">
<input type="number" id="txt-stroke-w" value="0" min="0" max="20" class="num-field" style="width:52px">
</div>
<label class="chk-row"><input type="checkbox" id="txt-shadow"> Drop shadow</label>
<input type="text" class="input-field" id="txt-content" placeholder="Type your text…" style="margin-bottom:8px">
<button class="p-btn acc full" id="btn-add-text">+ Add Text</button>
</div>
<!-- BACKGROUND -->
<div class="panel-section" id="sec-background">
<div class="sec-label">Solid Color</div>
<div class="bg-colors" id="bg-palette"></div>
<div class="fc-row" style="margin-bottom:14px">
<span class="fc-label">Custom</span>
<input type="color" id="bg-color-pick" value="#ffffff">
<button class="p-btn" id="btn-apply-bg" style="flex:1">Apply</button>
</div>
<div class="sec-label">Gradient</div>
<div class="sub-section">
<div class="fc-row">
<span class="fc-label">From</span>
<input type="color" id="grad-from" value="#667eea">
<span class="fc-label">To</span>
<input type="color" id="grad-to" value="#764ba2">
</div>
<select id="grad-dir" style="margin-bottom:8px">
<option value="h">→ Horizontal</option>
<option value="v">↓ Vertical</option>
<option value="dr">↘ Diagonal ↘</option>
<option value="ur">↗ Diagonal ↗</option>
</select>
<button class="p-btn full" id="btn-apply-grad">Apply Gradient</button>
</div>
<div class="sec-label">Presets</div>
<div class="grad-presets-grid" id="grad-presets"></div>
<div class="sec-label">Transparency</div>
<button class="p-btn full" id="btn-transparent-bg">Set Transparent</button>
</div>
<!-- LAYERS -->
<div class="panel-section" id="sec-layers">
<div id="layers-list"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-top:8px">
<button class="p-btn" id="lay-up">↑ Forward</button>
<button class="p-btn" id="lay-dn">↓ Backward</button>
<button class="p-btn" id="lay-dup">⎘ Duplicate</button>
<button class="p-btn danger" id="lay-del">✕ Delete</button>
</div>
</div>
</div>
</div>
<!-- CANVAS -->
<div id="canvas-wrap">
<div id="onboard">
<div class="ob-icon">🎨</div>
<div class="ob-title">Start your design</div>
<div class="ob-actions">
<button class="p-btn acc" onclick="document.getElementById('file-input').click()">Upload image</button>
<button class="p-btn" id="btn-sample-quick">Use sample</button>
</div>
</div>
<div id="canvas-toolbar">
<button class="ct-btn" id="ct-crop"></button>
<button class="ct-btn" id="ct-flip-h"></button>
<button class="ct-btn" id="ct-flip-v"></button>
<div class="ct-sep"></div>
<button class="ct-btn" id="ct-rot-l"></button>
<button class="ct-btn" id="ct-rot-r"></button>
<div class="ct-sep"></div>
<button class="ct-btn" id="ct-bring-f"></button>
<button class="ct-btn" id="ct-send-b"></button>
<div class="ct-sep"></div>
<button class="ct-btn" id="ct-del" style="color:var(--red)">🗑</button>
</div>
<div id="canvas-container">
<canvas id="main-canvas"></canvas>
<div id="crop-overlay">
<div id="crop-box">
<div class="crop-handle ch-tl"></div><div class="crop-handle ch-tr"></div>
<div class="crop-handle ch-bl"></div><div class="crop-handle ch-br"></div>
<div class="crop-handle ch-tm"></div><div class="crop-handle ch-bm"></div>
<div class="crop-handle ch-ml"></div><div class="crop-handle ch-mr"></div>
</div>
<div id="crop-toolbar">
<button class="p-btn acc" id="btn-crop-apply">✓ Apply</button>
<button class="p-btn" id="btn-crop-cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- RIGHT PANEL -->
<div id="right-panel">
<div class="rp-section" id="rp-pos">
<div class="rp-title">Position &amp; Size</div>
<div class="pos-grid">
<div><div class="dim-label">X</div><input type="number" class="dim-input" id="pos-x"></div>
<div><div class="dim-label">Y</div><input type="number" class="dim-input" id="pos-y"></div>
<div><div class="dim-label">W</div><input type="number" class="dim-input" id="size-w"></div>
<div><div class="dim-label">H</div><input type="number" class="dim-input" id="size-h"></div>
</div>
<label class="chk-row"><input type="checkbox" id="lock-ratio" checked> Lock aspect ratio</label>
<div class="fc-row">
<span class="fc-label">Rotate</span>
<input type="range" id="obj-rotate" min="-180" max="180" step="1" value="0">
<span class="fc-val" id="rot-val"></span>
</div>
<div class="fc-row">
<span class="fc-label">Opacity</span>
<input type="range" id="obj-opacity" min="0" max="1" step="0.01" value="1">
<span class="fc-val" id="op-val">100%</span>
</div>
</div>
<div class="rp-section" id="rp-adj">
<div class="rp-title">Adjustments</div>
<div class="fc-row"><span class="fc-label">Brightness</span><input type="range" id="adj-brightness" min="-1" max="1" step="0.01" value="0"><span class="fc-val" id="bri-val">0</span></div>
<div class="fc-row"><span class="fc-label">Contrast</span><input type="range" id="adj-contrast" min="-1" max="1" step="0.01" value="0"><span class="fc-val" id="con-val">0</span></div>
<div class="fc-row"><span class="fc-label">Saturation</span><input type="range" id="adj-saturation" min="-1" max="1" step="0.01" value="0"><span class="fc-val" id="sat-val">0</span></div>
<div class="fc-row"><span class="fc-label">Blur</span><input type="range" id="adj-blur" min="0" max="20" step="0.5" value="0"><span class="fc-val" id="blur-val">0</span></div>
<div class="sec-label" style="margin-top:6px;margin-bottom:6px">Quick Filters</div>
<div class="effect-list" id="filter-chips">
<span class="ef-chip active" data-filter="none">None</span>
<span class="ef-chip" data-filter="grayscale">B&amp;W</span>
<span class="ef-chip" data-filter="sepia">Sepia</span>
<span class="ef-chip" data-filter="invert">Invert</span>
<span class="ef-chip" data-filter="vintage">Vintage</span>
<span class="ef-chip" data-filter="polaroid">Polaroid</span>
<span class="ef-chip" data-filter="kodachrome">Kodachrome</span>
<span class="ef-chip" data-filter="brownie">Brownie</span>
<span class="ef-chip" data-filter="sharpen">Sharpen</span>
</div>
</div>
<div class="rp-section">
<div class="rp-title">Transform</div>
<div class="xform-grid">
<button class="xf-btn" id="xf-flip-h"></button>
<button class="xf-btn" id="xf-flip-v"></button>
<button class="xf-btn" id="xf-rot-l"></button>
<button class="xf-btn" id="xf-rot-r"></button>
</div>
</div>
<div class="rp-section">
<div class="rp-title">Canvas</div>
<div class="dim-group">
<div><div class="dim-label">Width</div><input type="number" class="dim-input" id="canvas-w" value="800"></div>
<div><div class="dim-label">Height</div><input type="number" class="dim-input" id="canvas-h" value="600"></div>
</div>
<button class="p-btn full" id="btn-apply-canvas">Apply</button>
</div>
<div class="rp-section">
<div class="rp-title">Quick Export</div>
<div class="fmt-cards" style="grid-template-columns:repeat(3,1fr);gap:5px">
<div class="fmt-card sel" data-fmt="webp"><div class="fmt-name">WebP</div><div class="fmt-sub">Best</div></div>
<div class="fmt-card" data-fmt="png"><div class="fmt-name">PNG</div><div class="fmt-sub">Lossless</div></div>
<div class="fmt-card" data-fmt="jpeg"><div class="fmt-name">JPG</div><div class="fmt-sub">Small</div></div>
</div>
<div class="fc-row">
<span class="fc-label">Quality</span>
<input type="range" id="exp-quality" min="0.1" max="1" step="0.01" value="0.9">
<span class="fc-val" id="q-val">90%</span>
</div>
<input type="text" class="input-field" id="fname" value="design" placeholder="filename" style="margin-bottom:6px">
<button class="p-btn acc full" id="btn-export">⬇ Download</button>
<button class="p-btn full" id="btn-copy-img" style="margin-top:5px">⎘ Copy to clipboard</button>
</div>
<div class="rp-section" id="rp-info">
<div class="rp-title">Info</div>
<div id="img-info" style="font-size:11px;font-family:var(--mono);color:var(--txt3);line-height:2">No selection</div>
</div>
</div>
</div>
<div id="statusbar">
<div class="sb-item"><div class="sb-dot" id="sb-dot"></div><span id="sb-status">Ready</span></div>
<div class="sb-sep"></div>
<div class="sb-item">Canvas: <strong id="sb-canvas">800×600</strong></div>
<div class="sb-sep"></div>
<div class="sb-item">Objects: <strong id="sb-objs">0</strong></div>
<div class="sb-sep"></div>
<div class="sb-item" id="sb-pos">Cursor: —</div>
<div style="margin-left:auto;opacity:.35">DesignForge Pro</div>
</div>
<!-- EXPORT MODAL -->
<div id="modal-overlay">
<div id="export-modal">
<div class="modal-header"><div class="modal-title">Export / Download</div><button class="modal-close" id="modal-close">×</button></div>
<div class="modal-body">
<div class="rp-title" style="margin-bottom:10px">Format</div>
<div class="fmt-cards">
<div class="fmt-card sel" data-fmt="webp"><div class="fmt-name">WebP</div><div class="fmt-sub">Best quality/size</div></div>
<div class="fmt-card" data-fmt="png"><div class="fmt-name">PNG</div><div class="fmt-sub">Lossless + alpha</div></div>
<div class="fmt-card" data-fmt="jpeg"><div class="fmt-name">JPG</div><div class="fmt-sub">Smallest</div></div>
<div class="fmt-card" data-fmt="svg"><div class="fmt-name">SVG</div><div class="fmt-sub">Vector</div></div>
</div>
<div class="fc-row" style="margin-bottom:12px">
<span class="fc-label">Quality</span>
<input type="range" id="modal-quality" min="0.1" max="1" step="0.01" value="0.9">
<span class="fc-val" id="modal-q-val">90%</span>
</div>
<div class="fc-row" style="margin-bottom:12px">
<span class="fc-label">Scale</span>
<input type="range" id="modal-scale" min="0.5" max="4" step="0.5" value="1">
<span class="fc-val" id="modal-scale-val"></span>
</div>
<div class="fc-row" style="margin-top:8px">
<span class="fc-label">Filename</span>
<input type="text" id="modal-fname" value="design" class="input-field" style="flex:1;margin:0">
</div>
</div>
<div class="modal-footer">
<button class="p-btn" id="modal-cancel">Cancel</button>
<button class="p-btn acc" id="btn-modal-export">⬇ Download</button>
</div>
</div>
</div>
<!-- SIZE MODAL -->
<div id="size-modal-overlay">
<div id="size-modal">
<div class="modal-header"><div class="modal-title">Resize Canvas</div><button class="modal-close" id="size-modal-close">×</button></div>
<div class="modal-body">
<div class="rp-title" style="margin-bottom:10px">Common Sizes</div>
<div class="size-presets" id="size-presets"></div>
<div class="rp-title" style="margin-bottom:8px">Custom</div>
<div class="dim-group">
<div><div class="dim-label">Width (px)</div><input type="number" class="dim-input" id="custom-w" value="800"></div>
<div><div class="dim-label">Height (px)</div><input type="number" class="dim-input" id="custom-h" value="600"></div>
</div>
</div>
<div class="modal-footer">
<button class="p-btn" id="size-cancel">Cancel</button>
<button class="p-btn acc" id="btn-apply-size">Apply Size</button>
</div>
</div>
</div>
<!-- CTX MENU -->
<div id="ctx-menu">
<div class="ctx-item" id="ctx-bring-f"><span class="ctx-icon"></span> Bring Forward</div>
<div class="ctx-item" id="ctx-send-b"><span class="ctx-icon"></span> Send Backward</div>
<div class="ctx-item" id="ctx-bring-front"><span class="ctx-icon"></span> Bring to Front</div>
<div class="ctx-item" id="ctx-send-back"><span class="ctx-icon"></span> Send to Back</div>
<div class="ctx-sep"></div>
<div class="ctx-item" id="ctx-dup"><span class="ctx-icon"></span> Duplicate</div>
<div class="ctx-item" id="ctx-flip-h"><span class="ctx-icon"></span> Flip Horizontal</div>
<div class="ctx-item" id="ctx-flip-v"><span class="ctx-icon"></span> Flip Vertical</div>
<div class="ctx-sep"></div>
<div class="ctx-item danger" id="ctx-del"><span class="ctx-icon">🗑</span> Delete</div>
</div>
<script>
'use strict';
// ============================================================
// SHAPE LIBRARY — fully inline SVG paths, no external fetch
// All paths are 24×24 viewBox (standard icon grid)
// ============================================================
const SHAPE_LIBRARY = [
{
cat: 'Basic Shapes',
shapes: [
{ id: 'rect', name: 'Rectangle', fabricType: 'rect', w:120, h:80 },
{ id: 'square', name: 'Square', fabricType: 'rect', w:90, h:90 },
{ id: 'roundrect', name: 'RoundRect', fabricType: 'rect', w:120, h:80, rx:14 },
{ id: 'circle', name: 'Circle', fabricType: 'ellipse', rx:55, ry:55 },
{ id: 'ellipse', name: 'Ellipse', fabricType: 'ellipse', rx:70, ry:45 },
{ id: 'line', name: 'Line', fabricType: 'line' },
{ id: 'hline', name: 'H-Line', fabricType: 'line-h' },
{ id: 'vline', name: 'V-Line', fabricType: 'line-v' },
{
id: 'triangle', name: 'Triangle', fabricType: 'path',
d: 'M12 3 L22 21 L2 21 Z',
preview: '<polygon points="12,3 22,21 2,21" fill="currentColor"/>'
},
{
id: 'tri-right', name: 'Rt Triangle', fabricType: 'path',
d: 'M3 3 L21 21 L3 21 Z',
preview: '<polygon points="3,3 21,21 3,21" fill="currentColor"/>'
},
{
id: 'tri-down', name: 'Tri Down', fabricType: 'path',
d: 'M2 3 L22 3 L12 21 Z',
preview: '<polygon points="2,3 22,3 12,21" fill="currentColor"/>'
},
]
},
{
cat: 'Polygons',
shapes: [
{
id: 'pentagon', name: 'Pentagon', fabricType: 'path',
d: 'M12 2 L21.8 9.1 L18 20.9 L6 20.9 L2.2 9.1 Z',
preview: '<polygon points="12,2 21.8,9.1 18,20.9 6,20.9 2.2,9.1" fill="currentColor"/>'
},
{
id: 'hexagon', name: 'Hexagon', fabricType: 'path',
d: 'M12 2 L20.66 7 L20.66 17 L12 22 L3.34 17 L3.34 7 Z',
preview: '<polygon points="12,2 20.66,7 20.66,17 12,22 3.34,17 3.34,7" fill="currentColor"/>'
},
{
id: 'octagon', name: 'Octagon', fabricType: 'path',
d: 'M8.1 2 L15.9 2 L22 8.1 L22 15.9 L15.9 22 L8.1 22 L2 15.9 L2 8.1 Z',
preview: '<polygon points="8.1,2 15.9,2 22,8.1 22,15.9 15.9,22 8.1,22 2,15.9 2,8.1" fill="currentColor"/>'
},
{
id: 'diamond', name: 'Diamond', fabricType: 'path',
d: 'M12 2 L22 12 L12 22 L2 12 Z',
preview: '<polygon points="12,2 22,12 12,22 2,12" fill="currentColor"/>'
},
{
id: 'parallelogram', name: 'Parallelogram', fabricType: 'path',
d: 'M6 4 L22 4 L18 20 L2 20 Z',
preview: '<polygon points="6,4 22,4 18,20 2,20" fill="currentColor"/>'
},
{
id: 'trapezoid', name: 'Trapezoid', fabricType: 'path',
d: 'M5 4 L19 4 L22 20 L2 20 Z',
preview: '<polygon points="5,4 19,4 22,20 2,20" fill="currentColor"/>'
},
]
},
{
cat: 'Stars & Bursts',
shapes: [
{
id: 'star5', name: 'Star 5pt', fabricType: 'path',
d: 'M12 2 L14.39 9.26 L22 9.27 L16.19 13.97 L18.36 21.27 L12 16.77 L5.64 21.27 L7.81 13.97 L2 9.27 L9.61 9.26 Z',
preview: '<polygon points="12,2 14.39,9.26 22,9.27 16.19,13.97 18.36,21.27 12,16.77 5.64,21.27 7.81,13.97 2,9.27 9.61,9.26" fill="currentColor"/>'
},
{
id: 'star6', name: 'Star 6pt', fabricType: 'path',
d: 'M12 2 L14 8 L20 8 L15 12 L17 18 L12 14.5 L7 18 L9 12 L4 8 L10 8 Z',
preview: '<polygon points="12,2 14,8 20,8 15,12 17,18 12,14.5 7,18 9,12 4,8 10,8" fill="currentColor"/>'
},
{
id: 'star4', name: 'Star 4pt', fabricType: 'path',
d: 'M12 2 L13.5 10.5 L22 12 L13.5 13.5 L12 22 L10.5 13.5 L2 12 L10.5 10.5 Z',
preview: '<polygon points="12,2 13.5,10.5 22,12 13.5,13.5 12,22 10.5,13.5 2,12 10.5,10.5" fill="currentColor"/>'
},
{
id: 'starburst', name: 'Starburst', fabricType: 'path',
d: 'M12 2L13.1 6.2L17 4L15.3 8L20 8L16.5 11L20 14L15.3 14L17 18L13.1 15.8L12 20L10.9 15.8L7 18L8.7 14L4 14L7.5 11L4 8L8.7 8L7 4L10.9 6.2Z',
preview: '<path d="M12 2L13.1 6.2L17 4L15.3 8L20 8L16.5 11L20 14L15.3 14L17 18L13.1 15.8L12 20L10.9 15.8L7 18L8.7 14L4 14L7.5 11L4 8L8.7 8L7 4L10.9 6.2Z" fill="currentColor"/>'
},
]
},
{
cat: 'Arrows',
shapes: [
{
id: 'arrow-r', name: 'Arrow →', fabricType: 'path',
d: 'M4 11 L16 11 L16 8 L22 12 L16 16 L16 13 L4 13 Z',
preview: '<path d="M4 11 L16 11 L16 8 L22 12 L16 16 L16 13 L4 13 Z" fill="currentColor"/>'
},
{
id: 'arrow-l', name: 'Arrow ←', fabricType: 'path',
d: 'M20 11 L8 11 L8 8 L2 12 L8 16 L8 13 L20 13 Z',
preview: '<path d="M20 11 L8 11 L8 8 L2 12 L8 16 L8 13 L20 13 Z" fill="currentColor"/>'
},
{
id: 'arrow-u', name: 'Arrow ↑', fabricType: 'path',
d: 'M13 20 L13 8 L16 8 L12 2 L8 8 L11 8 L11 20 Z',
preview: '<path d="M13 20 L13 8 L16 8 L12 2 L8 8 L11 8 L11 20 Z" fill="currentColor"/>'
},
{
id: 'arrow-d', name: 'Arrow ↓', fabricType: 'path',
d: 'M11 4 L11 16 L8 16 L12 22 L16 16 L13 16 L13 4 Z',
preview: '<path d="M11 4 L11 16 L8 16 L12 22 L16 16 L13 16 L13 4 Z" fill="currentColor"/>'
},
{
id: 'dbarrow', name: 'Double →←', fabricType: 'path',
d: 'M2 12 L7 8 L7 11 L17 11 L17 8 L22 12 L17 16 L17 13 L7 13 L7 16 Z',
preview: '<path d="M2 12 L7 8 L7 11 L17 11 L17 8 L22 12 L17 16 L17 13 L7 13 L7 16 Z" fill="currentColor"/>'
},
{
id: 'arrow-ne', name: 'Arrow ↗', fabricType: 'path',
d: 'M5 19 L16 8 L13 8 L16 5 L21 5 L21 10 L18 7 L7 18 Z',
preview: '<path d="M5 19 L16 8 L13 8 L16 5 L21 5 L21 10 L18 7 L7 18 Z" fill="currentColor"/>'
},
{
id: 'arrow-curved', name: 'Curved ↩', fabricType: 'path',
d: 'M12 5 C7 5 3 9 3 14 L3 17 L6 14 L3 11 L3 14 C3 10.7 7.1 8 12 8 C16.9 8 21 10.7 21 14 L21 15 L21 17 C21 13 17 9 12 5Z',
preview: '<path d="M12 5 C7 5 3 9 3 14 C3 10.7 7.1 8 12 8 C16.9 8 21 10.7 21 14" stroke="currentColor" stroke-width="2.5" fill="none"/>'
},
]
},
{
cat: 'Symbols & Icons',
shapes: [
{
id: 'heart', name: 'Heart', fabricType: 'path',
d: 'M12 21.593C10.76 20.525 2 14.252 2 8.535 2 5.48 4.315 3 7.163 3c1.658 0 3.084.89 3.837 2.186h2C13.753 3.89 15.179 3 16.837 3 19.685 3 22 5.48 22 8.535c0 5.717-8.76 11.99-10 13.058z',
preview: '<path d="M12 21.593C10.76 20.525 2 14.252 2 8.535 2 5.48 4.315 3 7.163 3c1.658 0 3.084.89 3.837 2.186h2C13.753 3.89 15.179 3 16.837 3 19.685 3 22 5.48 22 8.535c0 5.717-8.76 11.99-10 13.058z" fill="currentColor"/>'
},
{
id: 'star-filled', name: 'Star ★', fabricType: 'path',
d: 'M12 2L14.39 9.26L22 9.27L16.19 13.97L18.36 21.27L12 16.77L5.64 21.27L7.81 13.97L2 9.27L9.61 9.26Z',
preview: '<polygon points="12,2 14.39,9.26 22,9.27 16.19,13.97 18.36,21.27 12,16.77 5.64,21.27 7.81,13.97 2,9.27 9.61,9.26" fill="currentColor"/>'
},
{
id: 'checkmark', name: 'Checkmark', fabricType: 'path',
d: 'M4 12 L9 17 L20 6',
stroke: true,
preview: '<path d="M4 12 L9 17 L20 6" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>'
},
{
id: 'cross', name: 'Cross ×', fabricType: 'path',
d: 'M6 6 L18 18 M18 6 L6 18',
stroke: true,
preview: '<path d="M6 6 L18 18 M18 6 L6 18" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
},
{
id: 'plus', name: 'Plus +', fabricType: 'path',
d: 'M12 4 L12 20 M4 12 L20 12',
stroke: true,
preview: '<path d="M12 4 L12 20 M4 12 L20 12" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
},
{
id: 'speech', name: 'Speech', fabricType: 'path',
d: 'M4 4 L20 4 L20 15 L14 15 L12 20 L10 15 L4 15 Z',
preview: '<path d="M4 4 L20 4 L20 15 L14 15 L12 20 L10 15 L4 15 Z" fill="currentColor"/>'
},
{
id: 'cloud', name: 'Cloud', fabricType: 'path',
d: 'M6.5 20C4.57 20 3 18.43 3 16.5S4.57 13 6.5 13l.5.05A5.5 5.5 0 0 1 12 7a5.5 5.5 0 0 1 5 8.94.5.5 0 0 1-.03.06A3.5 3.5 0 0 1 19.5 20Z',
preview: '<path d="M6.5 20C4.57 20 3 18.43 3 16.5S4.57 13 6.5 13l.5.05A5.5 5.5 0 0 1 12 7a5.5 5.5 0 0 1 5 8.94A3.5 3.5 0 0 1 19.5 20Z" fill="currentColor"/>'
},
{
id: 'lightning', name: 'Lightning', fabricType: 'path',
d: 'M13 2 L5 14 L11 14 L11 22 L19 10 L13 10 Z',
preview: '<path d="M13 2 L5 14 L11 14 L11 22 L19 10 L13 10 Z" fill="currentColor"/>'
},
{
id: 'ban', name: 'Ban / No', fabricType: 'path',
d: 'M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2zM4.93 4.93l14.14 14.14',
stroke: true, fill: false,
preview: '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07" stroke="currentColor" stroke-width="2"/>'
},
{
id: 'bookmark', name: 'Bookmark', fabricType: 'path',
d: 'M5 3 L19 3 L19 22 L12 17 L5 22 Z',
preview: '<path d="M5 3 L19 3 L19 22 L12 17 L5 22 Z" fill="currentColor"/>'
},
{
id: 'shield', name: 'Shield', fabricType: 'path',
d: 'M12 2 L20 6 L20 13 C20 17.4 16.4 21.3 12 22 C7.6 21.3 4 17.4 4 13 L4 6 Z',
preview: '<path d="M12 2 L20 6 L20 13 C20 17.4 16.4 21.3 12 22 C7.6 21.3 4 17.4 4 13 L4 6 Z" fill="currentColor"/>'
},
{
id: 'location', name: 'Location', fabricType: 'path',
d: 'M12 2 C8.13 2 5 5.13 5 9 C5 14.25 12 22 12 22 C12 22 19 14.25 19 9 C19 5.13 15.87 2 12 2 Z M12 11.5 C10.62 11.5 9.5 10.38 9.5 9 C9.5 7.62 10.62 6.5 12 6.5 C13.38 6.5 14.5 7.62 14.5 9 C14.5 10.38 13.38 11.5 12 11.5 Z',
preview: '<path d="M12 2 C8.13 2 5 5.13 5 9 C5 14.25 12 22 12 22 C12 22 19 14.25 19 9 C19 5.13 15.87 2 12 2 Z" fill="currentColor"/>'
},
]
},
{
cat: 'Organic & Decorative',
shapes: [
{
id: 'crescent', name: 'Crescent', fabricType: 'path',
d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z',
preview: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="currentColor"/>'
},
{
id: 'sun', name: 'Sun', fabricType: 'path',
d: 'M12 17A5 5 0 1 0 12 7a5 5 0 0 0 0 10zm0-15v2m0 14v2M4.93 4.93l1.41 1.41m9.9 9.9 1.41 1.41M2 12h2m14 0h2M4.93 19.07l1.41-1.41m9.9-9.9 1.41-1.41',
stroke: true, fill: false,
preview: '<circle cx="12" cy="12" r="5" fill="currentColor"/><g stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></g>'
},
{
id: 'droplet', name: 'Droplet', fabricType: 'path',
d: 'M12 2 C12 2 5 10 5 15 A7 7 0 0 0 19 15 C19 10 12 2 12 2 Z',
preview: '<path d="M12 2 C12 2 5 10 5 15 A7 7 0 0 0 19 15 C19 10 12 2 12 2 Z" fill="currentColor"/>'
},
{
id: 'leaf', name: 'Leaf', fabricType: 'path',
d: 'M17 8 C17 8 20 12 18 17 C16 22 11 22 8 19 C5 16 4 11 7 8 C10 5 15 4 17 8 Z M12 12 L7 19',
preview: '<path d="M17 8 C17 8 20 12 18 17 C16 22 11 22 8 19 C5 16 4 11 7 8 C10 5 15 4 17 8 Z" fill="currentColor"/>'
},
{
id: 'blob1', name: 'Blob 1', fabricType: 'path',
d: 'M16.5 4.5 C20 7 22 11 20.5 15.5 C19 20 14.5 22 10 21 C5.5 20 2 16 2.5 11.5 C3 7 7 3 11.5 2.5 C14 2.2 15.5 3.5 16.5 4.5 Z',
preview: '<path d="M16.5 4.5 C20 7 22 11 20.5 15.5 C19 20 14.5 22 10 21 C5.5 20 2 16 2.5 11.5 C3 7 7 3 11.5 2.5 C14 2.2 15.5 3.5 16.5 4.5 Z" fill="currentColor"/>'
},
{
id: 'blob2', name: 'Blob 2', fabricType: 'path',
d: 'M15 3 C19 4 22 8.5 21.5 13 C21 17.5 17.5 21 13 21.5 C8.5 22 4 19 3 15 C2 11 4.5 6 8 4 C10.5 2.5 13 2.5 15 3 Z',
preview: '<path d="M15 3 C19 4 22 8.5 21.5 13 C21 17.5 17.5 21 13 21.5 C8.5 22 4 19 3 15 C2 11 4.5 6 8 4 C10.5 2.5 13 2.5 15 3 Z" fill="currentColor"/>'
},
{
id: 'flower', name: 'Flower', fabricType: 'path',
d: 'M12 7 C12 7 10 4 7 4 C4 4 4 7 6 9 C8 11 7 12 7 12 C7 12 4 12 3 14 C2 16 4 18 7 17 C10 16 12 17 12 17 C12 17 11 20 13 21 C15 22 17 20 16 17 C15 14 17 13 17 13 C17 13 20 14 21 12 C22 10 20 8 17 9 C14 10 12 7 12 7 Z',
preview: '<circle cx="12" cy="12" r="4" fill="currentColor"/><circle cx="12" cy="5" r="3" fill="currentColor" opacity=".7"/><circle cx="12" cy="19" r="3" fill="currentColor" opacity=".7"/><circle cx="5" cy="12" r="3" fill="currentColor" opacity=".7"/><circle cx="19" cy="12" r="3" fill="currentColor" opacity=".7"/>'
},
]
},
{
cat: 'Lines & Dividers',
shapes: [
{ id: 'line-s', name: 'Solid Line', fabricType: 'line-h', dash: null },
{ id: 'line-d', name: 'Dashed Line', fabricType: 'line-h', dash: [8,4] },
{ id: 'line-dt', name: 'Dotted Line', fabricType: 'line-h', dash: [2,4] },
{ id: 'line-v2', name: 'Vert. Line', fabricType: 'line-v' },
{ id: 'line-diag',name:'Diagonal', fabricType: 'line-diag' },
{
id: 'wave', name: 'Wave', fabricType: 'path',
d: 'M2 12 C4 8 6 8 8 12 C10 16 12 16 14 12 C16 8 18 8 20 12 C22 16 24 16 24 12',
stroke: true, fill: false,
preview: '<path d="M2 12 C4 8 6 8 8 12 C10 16 12 16 14 12 C16 8 18 8 20 12" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
},
]
},
];
// ============================================================
// HELPERS
// ============================================================
const $i = id => document.getElementById(id);
const qs = (s, ctx) => (ctx || document).querySelector(s);
const qsa = (s, ctx) => [...(ctx || document).querySelectorAll(s)];
function toast(msg, type) {
const t = $i('toast');
t.textContent = msg;
t.className = 'show' + (type === 'err' ? ' err' : '');
clearTimeout(t._t);
t._t = setTimeout(() => t.className = '', 2800);
}
function setStatus(msg, warn) {
$i('sb-status').textContent = msg;
$i('sb-dot').className = 'sb-dot' + (warn ? ' warn' : '');
}
// ============================================================
// STATE
// ============================================================
let canvas, zoom = 1, fmt = 'webp', exportQuality = 0.9, exportScale = 1;
let hist = [], hidx = -1, undoing = false;
let boldOn = false, italicOn = false, underlineOn = false, textAlign = 'left';
let cropActive = false, copiedObject = null, activeFilter = 'none';
let cropState = {};
// ============================================================
// CANVAS INIT
// ============================================================
function initCanvas(w, h) {
w = w || 800; h = h || 600;
if (canvas) canvas.dispose();
canvas = new fabric.Canvas('main-canvas', {
width: w, height: h, backgroundColor: '#ffffff', preserveObjectStacking: true, selection: true
});
canvas.on('object:added', onCanvasChange);
canvas.on('object:removed', onCanvasChange);
canvas.on('object:modified', onCanvasChange);
canvas.on('selection:created', onSelectionChange);
canvas.on('selection:updated', onSelectionChange);
canvas.on('selection:cleared', () => {
$i('canvas-toolbar').classList.remove('visible');
$i('img-info').textContent = 'No selection';
});
canvas.on('mouse:move', e => {
if (e.pointer) $i('sb-pos').textContent = `Cursor: ${Math.round(e.pointer.x)}, ${Math.round(e.pointer.y)}`;
});
canvas.on('mouse:down', e => {
if (e.e.button === 2) showCtxMenu(e.e, canvas.getActiveObject());
else hideCtxMenu();
});
$i('canvas-w').value = w;
$i('canvas-h').value = h;
$i('custom-w').value = w;
$i('custom-h').value = h;
updateSB();
applyZoom(zoom);
}
function onCanvasChange() { updateLayers(); updateSB(); saveHist(); }
function onSelectionChange() {
const obj = canvas.getActiveObject();
updateRightPanel(obj);
$i('canvas-toolbar').classList.add('visible');
updateLayers();
}
// ============================================================
// HISTORY
// ============================================================
function saveHist() {
if (undoing) return;
try {
const s = JSON.stringify(canvas.toJSON(['_name', '_type']));
hidx++;
hist = hist.slice(0, hidx);
hist.push(s);
if (hist.length > 60) { hist.shift(); hidx--; }
} catch (e) {}
}
function undo() {
if (hidx <= 0) { toast('Nothing to undo'); return; }
undoing = true; hidx--;
canvas.loadFromJSON(hist[hidx], () => { canvas.renderAll(); updateLayers(); updateSB(); undoing = false; toast('Undo'); });
}
function redo() {
if (hidx >= hist.length - 1) { toast('Nothing to redo'); return; }
undoing = true; hidx++;
canvas.loadFromJSON(hist[hidx], () => { canvas.renderAll(); updateLayers(); updateSB(); undoing = false; toast('Redo'); });
}
// ============================================================
// STATUS BAR
// ============================================================
function updateSB() {
$i('sb-canvas').textContent = canvas.width + '×' + canvas.height;
$i('sb-objs').textContent = canvas.getObjects().length;
$i('canvas-w').value = canvas.width;
$i('canvas-h').value = canvas.height;
}
// ============================================================
// ZOOM
// ============================================================
function applyZoom(z) {
zoom = Math.max(0.05, Math.min(8, z));
const cc = $i('canvas-container');
cc.style.transform = `scale(${zoom})`;
cc.style.transformOrigin = 'center center';
$i('zoom-pct').value = Math.round(zoom * 100) + '%';
}
function fitZoom() {
const wrap = $i('canvas-wrap');
const z = Math.min((wrap.clientWidth - 80) / canvas.width, (wrap.clientHeight - 80) / canvas.height, 1);
applyZoom(z);
}
// ============================================================
// ADD SHAPE — fully Fabric-based, no external SVG fetching
// ============================================================
function getShapeStyle() {
const noFill = $i('sh-no-fill').checked;
return {
fill: noFill ? 'transparent' : $i('sh-fill').value,
stroke: $i('sh-stroke').value,
strokeWidth: parseInt($i('sh-stroke-w').value) || 0,
opacity: parseFloat($i('sh-opacity').value)
};
}
function addShape(shapeObj) {
const style = getShapeStyle();
const cx = canvas.width / 2, cy = canvas.height / 2;
let obj;
const base = {
left: cx, top: cy,
originX: 'center', originY: 'center',
fill: style.fill,
stroke: style.strokeWidth > 0 ? style.stroke : null,
strokeWidth: style.strokeWidth,
opacity: style.opacity,
objectCaching: false
};
const ft = shapeObj.fabricType;
if (ft === 'rect') {
obj = new fabric.Rect({
...base,
width: shapeObj.w || 120,
height: shapeObj.h || 80,
rx: shapeObj.rx || 0,
ry: shapeObj.rx || 0
});
} else if (ft === 'ellipse') {
obj = new fabric.Ellipse({
...base,
rx: shapeObj.rx || 55,
ry: shapeObj.ry || 55
});
} else if (ft === 'line' || ft === 'line-h') {
const dash = shapeObj.dash !== undefined ? shapeObj.dash : null;
obj = new fabric.Line([cx - 80, cy, cx + 80, cy], {
stroke: style.stroke || '#111',
strokeWidth: Math.max(style.strokeWidth || 2, 1),
strokeDashArray: dash,
fill: null, opacity: style.opacity,
selectable: true, evented: true
});
obj.left = cx; obj.top = cy;
} else if (ft === 'line-v') {
obj = new fabric.Line([cx, cy - 80, cx, cy + 80], {
stroke: style.stroke || '#111',
strokeWidth: Math.max(style.strokeWidth || 2, 1),
fill: null, opacity: style.opacity
});
} else if (ft === 'line-diag') {
obj = new fabric.Line([cx - 60, cy - 60, cx + 60, cy + 60], {
stroke: style.stroke || '#111',
strokeWidth: Math.max(style.strokeWidth || 2, 1),
fill: null, opacity: style.opacity
});
} else if (ft === 'path' && shapeObj.d) {
// Determine effective fill/stroke for path
let pathFill = style.fill;
let pathStroke = style.strokeWidth > 0 ? style.stroke : null;
let pathStrokeW = style.strokeWidth;
// For stroke-only shapes (like checkmark, wave)
if (shapeObj.stroke && shapeObj.fill === false) {
pathFill = 'transparent';
pathStroke = style.strokeWidth > 0 ? style.stroke : style.fill; // use fill color as stroke if no stroke set
pathStrokeW = Math.max(style.strokeWidth || 2, 1.5);
}
obj = new fabric.Path(shapeObj.d, {
...base,
fill: pathFill,
stroke: pathStroke,
strokeWidth: pathStrokeW,
strokeLineCap: 'round',
strokeLineJoin: 'round',
scaleX: 3.5, scaleY: 3.5 // Lucide viewBox is 24×24 — scale up for canvas
});
} else {
toast('Unknown shape type', 'err');
return;
}
if (!obj) return;
obj._name = shapeObj.name;
obj._type = ft;
$i('onboard').style.display = 'none';
canvas.add(obj);
canvas.setActiveObject(obj);
canvas.renderAll();
toast(`${shapeObj.name} added`, 'ok');
saveHist();
}
// ============================================================
// BUILD SHAPE PANEL
// ============================================================
function buildShapePanel() {
const container = $i('shapes-container');
container.innerHTML = '';
SHAPE_LIBRARY.forEach(cat => {
const section = document.createElement('div');
section.className = 'shape-category';
section.dataset.cat = cat.cat.toLowerCase();
section.innerHTML = `<div class="shape-cat-label">${cat.cat}</div>`;
const grid = document.createElement('div');
grid.className = 'shape-grid';
cat.shapes.forEach(shapeObj => {
const btn = document.createElement('button');
btn.className = 'shape-btn';
btn.title = shapeObj.name;
btn.dataset.name = shapeObj.name.toLowerCase();
// Build SVG preview
let svgContent = '';
if (shapeObj.preview) {
svgContent = shapeObj.preview;
} else if (shapeObj.fabricType === 'rect') {
if (shapeObj.rx) svgContent = `<rect x="2" y="5" width="20" height="14" rx="${Math.round(shapeObj.rx/7)}" fill="currentColor"/>`;
else svgContent = `<rect x="2" y="5" width="20" height="14" fill="currentColor"/>`;
} else if (shapeObj.fabricType === 'ellipse') {
const scaleX = shapeObj.ry && shapeObj.rx ? (shapeObj.ry/shapeObj.rx) : 1;
if (Math.abs(scaleX - 1) < 0.15) svgContent = `<circle cx="12" cy="12" r="9" fill="currentColor"/>`;
else svgContent = `<ellipse cx="12" cy="12" rx="10" ry="7" fill="currentColor"/>`;
} else if (shapeObj.fabricType === 'line-h' || shapeObj.fabricType === 'line') {
const dash = shapeObj.dash;
let da = '';
if (dash) da = `stroke-dasharray="${dash.join(' ')}"`;
svgContent = `<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" ${da}/>`;
} else if (shapeObj.fabricType === 'line-v') {
svgContent = `<line x1="12" y1="2" x2="12" y2="22" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>`;
} else if (shapeObj.fabricType === 'line-diag') {
svgContent = `<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>`;
}
btn.innerHTML = `
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="color:var(--acc)">${svgContent}</svg>
<span class="s-label">${shapeObj.name}</span>
`;
btn.addEventListener('click', () => addShape(shapeObj));
grid.appendChild(btn);
});
section.appendChild(grid);
container.appendChild(section);
});
}
// Shape search
function setupShapeSearch() {
const input = $i('shape-search');
input.addEventListener('input', function() {
const q = this.value.trim().toLowerCase();
qsa('.shape-category').forEach(sec => {
const btns = qsa('.shape-btn', sec);
let visible = 0;
btns.forEach(btn => {
const match = !q || btn.dataset.name.includes(q) || sec.dataset.cat.includes(q);
btn.style.display = match ? '' : 'none';
if (match) visible++;
});
sec.style.display = visible ? '' : 'none';
});
});
}
// ============================================================
// TEXT
// ============================================================
const FONT_PRESETS = {
heading: { fontSize: 48, fontWeight: 'bold', fontFamily: 'Georgia', text: 'Heading' },
subheading: { fontSize: 28, fontWeight: '600', fontFamily: 'Georgia', text: 'Subheading' },
body: { fontSize: 16, fontWeight: 'normal', fontFamily: 'Arial', text: 'Body text' },
caption: { fontSize: 12, fontWeight: 'normal', fontFamily: 'Arial', text: 'Caption text', fill: '#888888' }
};
function addText(preset) {
const p = FONT_PRESETS[preset] || {};
const customText = $i('txt-content').value;
const text = preset === 'custom'
? (customText || 'Text')
: (p.text || 'Text');
const obj = new fabric.IText(text, {
left: canvas.width / 2, top: canvas.height / 2,
originX: 'center', originY: 'center',
fontFamily: preset === 'custom' ? ($i('txt-font').value || 'Arial') : p.fontFamily,
fontSize: preset === 'custom' ? (parseInt($i('txt-size').value) || 36) : p.fontSize,
fontWeight: preset === 'custom' ? (boldOn ? 'bold' : 'normal') : p.fontWeight,
fill: preset === 'custom' ? $i('txt-color').value : (p.fill || '#111111'),
textAlign: textAlign,
objectCaching: false
});
obj._name = text.slice(0, 12) || 'Text';
$i('onboard').style.display = 'none';
canvas.add(obj);
canvas.setActiveObject(obj);
canvas.renderAll();
toast('Text added', 'ok');
saveHist();
}
function updateActiveText(prop, val) {
const obj = canvas.getActiveObject();
if (obj && (obj.type === 'i-text' || obj.type === 'text')) {
obj.set(prop, val);
canvas.renderAll();
saveHist();
}
}
// ============================================================
// RIGHT PANEL
// ============================================================
function updateRightPanel(obj) {
if (!obj) { $i('img-info').textContent = 'No selection'; return; }
$i('pos-x').value = Math.round(obj.left);
$i('pos-y').value = Math.round(obj.top);
$i('size-w').value = Math.round(obj.getScaledWidth());
$i('size-h').value = Math.round(obj.getScaledHeight());
$i('obj-rotate').value = obj.angle || 0;
$i('rot-val').textContent = Math.round(obj.angle || 0) + '°';
$i('obj-opacity').value = obj.opacity !== undefined ? obj.opacity : 1;
$i('op-val').textContent = Math.round((obj.opacity !== undefined ? obj.opacity : 1) * 100) + '%';
$i('rp-adj').style.display = obj.type === 'image' ? '' : 'none';
if (obj.type === 'i-text' || obj.type === 'text') {
$i('txt-font').value = obj.fontFamily || 'Arial';
$i('txt-size').value = obj.fontSize || 36;
$i('txt-color').value = obj.fill || '#111111';
boldOn = obj.fontWeight === 'bold';
italicOn = obj.fontStyle === 'italic';
underlineOn = !!obj.underline;
$i('txt-bold').classList.toggle('active', boldOn);
$i('txt-italic').classList.toggle('active', italicOn);
$i('txt-underline').classList.toggle('active', underlineOn);
}
// Shape style sync
if (['rect','circle','ellipse','triangle','polygon','path','line'].includes(obj.type)) {
if ($i('sh-fill')) $i('sh-fill').value = (obj.fill && obj.fill !== 'transparent') ? obj.fill : '#7c3aed';
if ($i('sh-no-fill')) $i('sh-no-fill').checked = !obj.fill || obj.fill === 'transparent';
if ($i('sh-stroke') && obj.stroke) $i('sh-stroke').value = obj.stroke;
if ($i('sh-stroke-w')) $i('sh-stroke-w').value = obj.strokeWidth || 0;
if ($i('sh-opacity')) {
$i('sh-opacity').value = obj.opacity || 1;
$i('sh-op-val').textContent = Math.round((obj.opacity || 1) * 100) + '%';
}
}
$i('img-info').innerHTML = `Type: ${obj.type}<br>W: ${Math.round(obj.getScaledWidth())}px<br>H: ${Math.round(obj.getScaledHeight())}px<br>X: ${Math.round(obj.left)} Y: ${Math.round(obj.top)}`;
}
function updateActiveShape(prop, val) {
const obj = canvas.getActiveObject();
if (obj && ['rect','circle','ellipse','triangle','polygon','path','line'].includes(obj.type)) {
obj.set(prop, val);
canvas.renderAll();
saveHist();
}
}
// ============================================================
// LAYERS
// ============================================================
function updateLayers() {
const el = $i('layers-list');
const objs = canvas.getObjects();
const active = canvas.getActiveObject();
el.innerHTML = '';
const icons = { image:'🖼', 'i-text':'T', text:'T', rect:'▭', circle:'◯', ellipse:'◯', triangle:'△', line:'—', polygon:'⬠', path:'✏' };
for (let i = objs.length - 1; i >= 0; i--) {
const o = objs[i];
const ic = icons[o.type] || '◈';
const nm = o._name || (o.type === 'i-text' ? (o.text || '').slice(0, 14) : o.type);
const d = document.createElement('div');
d.className = 'layer-item' + (o === active ? ' active' : '');
d.innerHTML = `<span class="layer-icon">${ic}</span><span class="layer-name">${nm}</span><button class="layer-vis">👁</button>`;
d.onclick = function(e) {
if (e.target.classList.contains('layer-vis')) { o.visible = !o.visible; canvas.renderAll(); return; }
canvas.setActiveObject(o); canvas.renderAll(); updateLayers(); updateRightPanel(o);
};
el.appendChild(d);
}
}
// ============================================================
// IMAGE LOAD
// ============================================================
function loadURL(url) {
url = (url || '').trim();
if (!url.startsWith('http')) { toast('Enter a valid URL', 'err'); return; }
toast('Loading…');
setStatus('Loading…', true);
fabric.Image.fromURL(url, img => {
if (!img || !img.width) {
const proxy = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url);
fabric.Image.fromURL(proxy, img2 => {
if (!img2 || !img2.width) { toast('Could not load image', 'err'); setStatus('Load failed', true); return; }
placeImage(img2);
}, { crossOrigin: 'anonymous' });
return;
}
placeImage(img);
}, { crossOrigin: 'anonymous' });
}
function loadFile(file) {
const r = new FileReader();
r.onload = e => {
fabric.Image.fromURL(e.target.result, img => placeImage(img));
};
r.readAsDataURL(file);
}
function placeImage(img) {
$i('onboard').style.display = 'none';
const sc = Math.min(canvas.width * 0.9 / img.width, canvas.height * 0.9 / img.height, 1);
img.set({ left: canvas.width / 2, top: canvas.height / 2, originX: 'center', originY: 'center', scaleX: sc, scaleY: sc });
img._name = 'Image'; img._type = 'image';
canvas.add(img); canvas.setActiveObject(img); canvas.renderAll();
['adj-brightness','adj-contrast','adj-saturation','adj-blur'].forEach(id => { if ($i(id)) $i(id).value = 0; });
['bri-val','con-val','sat-val','blur-val'].forEach(id => { if ($i(id)) $i(id).textContent = '0'; });
qsa('#filter-chips .ef-chip').forEach(c => c.classList.remove('active'));
qs('#filter-chips [data-filter="none"]').classList.add('active');
activeFilter = 'none';
toast('Image loaded!', 'ok');
setStatus('Ready');
saveHist();
}
// ============================================================
// FILTERS
// ============================================================
function applyFilters() {
let img = canvas.getActiveObject();
if (!img || img.type !== 'image') { const imgs = canvas.getObjects('image'); img = imgs[imgs.length - 1]; }
if (!img) return;
img.filters = [];
const b = parseFloat($i('adj-brightness').value);
const c = parseFloat($i('adj-contrast').value);
const s = parseFloat($i('adj-saturation').value);
const bl = parseFloat($i('adj-blur').value);
if (b !== 0) img.filters.push(new fabric.Image.filters.Brightness({ brightness: b }));
if (c !== 0) img.filters.push(new fabric.Image.filters.Contrast({ contrast: c }));
if (s !== 0) img.filters.push(new fabric.Image.filters.Saturation({ saturation: s }));
if (bl > 0) img.filters.push(new fabric.Image.filters.Blur({ blur: bl / 20 }));
const F = fabric.Image.filters;
const fmap = { grayscale: () => new F.Grayscale(), sepia: () => new F.Sepia(), invert: () => new F.Invert(), vintage: () => new F.Vintage(), polaroid: () => new F.Polaroid(), kodachrome: () => new F.Kodachrome(), brownie: () => new F.Brownie(), sharpen: () => new F.Convolute({ matrix: [0,-1,0,-1,5,-1,0,-1,0] }) };
if (fmap[activeFilter]) img.filters.push(fmap[activeFilter]());
img.applyFilters();
canvas.renderAll();
}
// ============================================================
// BACKGROUND
// ============================================================
function setupBgPalette() {
const colors = ['#ffffff','#f8f8f8','#111111','#222222','#ef4444','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#14b8a6','#fef3c7','#dbeafe','#fce7f3','#d1fae5'];
const el = $i('bg-palette');
colors.forEach(c => {
const d = document.createElement('div');
d.className = 'bg-chip'; d.style.background = c; d.title = c;
d.onclick = () => { canvas.setBackgroundColor(c, canvas.renderAll.bind(canvas)); saveHist(); };
el.appendChild(d);
});
const tr = document.createElement('div');
tr.className = 'bg-chip';
tr.style.background = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Crect width='4' height='4' fill='%23ccc'/%3E%3Crect x='4' y='4' width='4' height='4' fill='%23ccc'/%3E%3C/svg%3E\")";
tr.title = 'Transparent';
tr.onclick = () => { canvas.setBackgroundColor(null, canvas.renderAll.bind(canvas)); saveHist(); };
el.appendChild(tr);
}
function setupGradPresets() {
const grads = [
{ label:'Sunset', a:'#f093fb', b:'#f5576c' },
{ label:'Ocean', a:'#4facfe', b:'#00f2fe' },
{ label:'Forest', a:'#43e97b', b:'#38f9d7' },
{ label:'Purple', a:'#667eea', b:'#764ba2' },
{ label:'Peach', a:'#ffecd2', b:'#fcb69f' },
{ label:'Night', a:'#0c0c0c', b:'#1a237e' },
{ label:'Fire', a:'#f7971e', b:'#ffd200' },
{ label:'Rose', a:'#f953c6', b:'#b91d73' },
];
const el = $i('grad-presets');
grads.forEach(g => {
const d = document.createElement('div');
d.className = 'grad-item';
d.style.background = `linear-gradient(135deg,${g.a},${g.b})`;
d.title = g.label;
d.onclick = () => applyFabricGradient(g.a, g.b, 'dr');
el.appendChild(d);
});
}
function applyFabricGradient(from, to, dir) {
const w = canvas.width, h = canvas.height;
const coords = {
h: { x1:0, y1:0, x2:w, y2:0 },
v: { x1:0, y1:0, x2:0, y2:h },
dr: { x1:0, y1:0, x2:w, y2:h },
ur: { x1:0, y1:h, x2:w, y2:0 }
}[dir] || { x1:0, y1:0, x2:w, y2:h };
const gradient = new fabric.Gradient({ type:'linear', coords, colorStops:[{ offset:0, color:from },{ offset:1, color:to }] });
canvas.setBackgroundColor(gradient, canvas.renderAll.bind(canvas));
saveHist();
toast('Gradient applied', 'ok');
}
// ============================================================
// CROP
// ============================================================
function startCrop() {
const obj = canvas.getActiveObject();
if (!obj || obj.type !== 'image') { toast('Select an image to crop', 'err'); return; }
cropActive = true; canvas.selection = false; canvas.discardActiveObject(); canvas.renderAll();
const el = $i('crop-overlay');
el.classList.add('active');
el.style.width = canvas.width + 'px';
el.style.height = canvas.height + 'px';
const il = obj.left - obj.getScaledWidth() / 2;
const it = obj.top - obj.getScaledHeight() / 2;
cropState = { x: Math.max(0, il), y: Math.max(0, it), w: Math.min(obj.getScaledWidth(), canvas.width), h: Math.min(obj.getScaledHeight(), canvas.height), obj };
updateCropBox();
}
function updateCropBox() {
const box = $i('crop-box');
box.style.cssText = `left:${cropState.x}px;top:${cropState.y}px;width:${cropState.w}px;height:${cropState.h}px`;
}
function applyCrop() {
const obj = cropState.obj;
if (!obj) { cancelCrop(); return; }
const imgLeft = obj.left - obj.getScaledWidth() / 2;
const imgTop = obj.top - obj.getScaledHeight() / 2;
const cropX = (cropState.x - imgLeft) / obj.scaleX;
const cropY = (cropState.y - imgTop) / obj.scaleY;
const cropW = Math.max(1, cropState.w / obj.scaleX);
const cropH = Math.max(1, cropState.h / obj.scaleY);
const tmp = document.createElement('canvas');
tmp.width = Math.round(cropW); tmp.height = Math.round(cropH);
tmp.getContext('2d').drawImage(obj.getElement(), Math.max(0, cropX), Math.max(0, cropY), cropW, cropH, 0, 0, cropW, cropH);
const idx = canvas.getObjects().indexOf(obj);
canvas.remove(obj);
fabric.Image.fromURL(tmp.toDataURL(), newImg => {
newImg.set({ left: cropState.x + cropState.w/2, top: cropState.y + cropState.h/2, originX:'center', originY:'center' });
newImg._name = 'Cropped';
canvas.insertAt(newImg, Math.max(0, idx));
canvas.setActiveObject(newImg); canvas.renderAll();
cancelCrop(); toast('Cropped!', 'ok'); saveHist();
});
}
function cancelCrop() {
cropActive = false; canvas.selection = true;
$i('crop-overlay').classList.remove('active');
}
// Crop drag
(function() {
const box = $i('crop-box');
let startX, startY, startState, handle;
box.addEventListener('mousedown', e => {
e.stopPropagation();
handle = e.target.classList.contains('crop-handle') ? [...e.target.classList].find(c => c.startsWith('ch-')) : 'move';
startX = e.clientX; startY = e.clientY;
startState = { ...cropState };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
function onMove(e) {
const dx = (e.clientX - startX) / zoom, dy = (e.clientY - startY) / zoom;
const s = startState;
if (handle === 'move') { cropState.x = Math.max(0, s.x + dx); cropState.y = Math.max(0, s.y + dy); }
else if (handle === 'ch-br') { cropState.w = Math.max(20, s.w + dx); cropState.h = Math.max(20, s.h + dy); }
else if (handle === 'ch-bl') { cropState.x = s.x + dx; cropState.w = Math.max(20, s.w - dx); cropState.h = Math.max(20, s.h + dy); }
else if (handle === 'ch-tr') { cropState.y = s.y + dy; cropState.w = Math.max(20, s.w + dx); cropState.h = Math.max(20, s.h - dy); }
else if (handle === 'ch-tl') { cropState.x = s.x + dx; cropState.y = s.y + dy; cropState.w = Math.max(20, s.w - dx); cropState.h = Math.max(20, s.h - dy); }
else if (handle === 'ch-mr') { cropState.w = Math.max(20, s.w + dx); }
else if (handle === 'ch-ml') { cropState.x = s.x + dx; cropState.w = Math.max(20, s.w - dx); }
else if (handle === 'ch-bm') { cropState.h = Math.max(20, s.h + dy); }
else if (handle === 'ch-tm') { cropState.y = s.y + dy; cropState.h = Math.max(20, s.h - dy); }
updateCropBox();
}
function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }
})();
// ============================================================
// CONTEXT MENU
// ============================================================
function showCtxMenu(e, obj) {
if (!obj) return;
const menu = $i('ctx-menu');
menu.style.display = 'block';
menu.style.left = Math.min(e.clientX, window.innerWidth - 170) + 'px';
menu.style.top = Math.min(e.clientY, window.innerHeight - 220) + 'px';
}
function hideCtxMenu() { $i('ctx-menu').style.display = 'none'; }
// ============================================================
// EXPORT
// ============================================================
function doExport(options) {
const { fmt: f = fmt, quality: q = exportQuality, scale = exportScale, filename: fn = ($i('fname').value || 'design') } = options || {};
const ext = f === 'jpeg' ? 'jpg' : f;
try {
const dataURL = canvas.toDataURL({ format: f, quality: q, multiplier: scale });
const a = document.createElement('a'); a.href = dataURL; a.download = fn + '.' + ext; a.click();
toast('Downloaded!', 'ok');
} catch (e) { toast('Export failed: ' + e.message, 'err'); }
}
async function doCopy() {
try {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 1 });
const blob = await (await fetch(dataURL)).blob();
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
toast('Copied!', 'ok');
} catch (e) { toast('Clipboard requires HTTPS', 'err'); }
}
// ============================================================
// CANVAS RESIZE
// ============================================================
const SIZE_PRESETS = [
{ name:'Custom', w:800, h:600 }, { name:'Square', w:500, h:500 }, { name:'HD 1080p', w:1920, h:1080 },
{ name:'Instagram', w:1080, h:1080 }, { name:'Facebook', w:1200, h:628 }, { name:'Twitter', w:1500, h:500 },
{ name:'YouTube', w:2560, h:1440 }, { name:'OG Image', w:1200, h:630 }, { name:'A4', w:2480, h:3508 },
{ name:'Banner', w:300, h:250 }, { name:'Leaderboard', w:728, h:90 }, { name:'Blog', w:800, h:450 }
];
function setupSizePresets() {
const el = $i('size-presets');
SIZE_PRESETS.forEach(p => {
const d = document.createElement('div');
d.className = 'size-preset';
d.innerHTML = `<div class="sp-name">${p.name}</div><div class="sp-dim">${p.w}×${p.h}</div>`;
d.onclick = () => {
qsa('.size-preset').forEach(x => x.classList.remove('sel'));
d.classList.add('sel'); $i('custom-w').value = p.w; $i('custom-h').value = p.h;
};
el.appendChild(d);
});
}
function applyCanvasSize(w, h) {
w = parseInt(w) || 800; h = parseInt(h) || 600;
canvas.setWidth(w); canvas.setHeight(h); canvas.renderAll();
updateSB(); fitZoom();
toast(`Canvas → ${w}×${h}`, 'ok'); saveHist();
}
function applyPosSize() {
const obj = canvas.getActiveObject();
if (!obj) return;
const x = parseFloat($i('pos-x').value), y = parseFloat($i('pos-y').value);
const w = parseFloat($i('size-w').value), h = parseFloat($i('size-h').value);
if (!isNaN(x)) obj.set('left', x);
if (!isNaN(y)) obj.set('top', y);
if (!isNaN(w) && w > 0) {
const sx = w / obj.width;
const sy = $i('lock-ratio').checked ? sx : (!isNaN(h) && h > 0 ? h / obj.height : obj.scaleY);
obj.set({ scaleX: sx, scaleY: sy });
}
obj.setCoords(); canvas.renderAll(); saveHist();
}
// ============================================================
// PANEL SWITCHING
// ============================================================
let activePanelId = 'upload';
function switchPanel(id) {
qsa('.tab-icon').forEach(t => t.classList.remove('active'));
qs(`[data-panel="${id}"]`).classList.add('active');
qsa('.panel-section').forEach(s => s.classList.remove('active'));
const sec = $i('sec-' + id);
if (sec) sec.classList.add('active');
const titles = { upload:'Upload', templates:'Templates', elements:'Shapes', text:'Text', background:'Background', layers:'Layers' };
$i('panel-title').textContent = titles[id] || id;
const panel = $i('left-panel');
if (activePanelId === id && panel.classList.contains('open')) panel.classList.remove('open');
else { panel.classList.add('open'); activePanelId = id; }
}
// ============================================================
// MAIN INIT
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
initCanvas(800, 600);
saveHist();
setupBgPalette();
setupGradPresets();
setupSizePresets();
buildShapePanel();
setupShapeSearch();
// Load Google Fonts
const fonts = ['Poppins','Montserrat','Raleway','Nunito','Inter','Space+Grotesk','Plus+Jakarta+Sans','Manrope','Sora','Playfair+Display','Lora','Merriweather','Cormorant','Abril+Fatface','Bebas+Neue','Pacifico','Dancing+Script','Caveat','Russo+One','JetBrains+Mono','Fira+Code','Space+Mono'];
const link = document.createElement('link');
link.href = `https://fonts.googleapis.com/css2?family=${fonts.join('&family=')}&display=swap`;
link.rel = 'stylesheet';
document.head.appendChild(link);
// TAB ICONS
qsa('.tab-icon[data-panel]').forEach(t => t.addEventListener('click', () => switchPanel(t.dataset.panel)));
$i('panel-close-btn').addEventListener('click', () => $i('left-panel').classList.remove('open'));
// UPLOAD
const uz = $i('upload-zone');
uz.addEventListener('click', () => $i('file-input').click());
uz.addEventListener('dragover', e => { e.preventDefault(); uz.classList.add('drag-over'); });
uz.addEventListener('dragleave', () => uz.classList.remove('drag-over'));
uz.addEventListener('drop', e => { e.preventDefault(); uz.classList.remove('drag-over'); if (e.dataTransfer.files[0]?.type.startsWith('image/')) loadFile(e.dataTransfer.files[0]); });
$i('file-input').addEventListener('change', e => { if (e.target.files[0]) loadFile(e.target.files[0]); });
// URL
$i('btn-load-url').addEventListener('click', () => loadURL($i('url-input').value));
$i('url-input').addEventListener('keydown', e => { if (e.key === 'Enter') loadURL(e.target.value); });
// SAMPLES
qsa('#sample-grid .thumb-item').forEach(t => t.addEventListener('click', () => loadURL(t.dataset.url)));
$i('btn-sample-quick').addEventListener('click', () => {
const urls = ['abc','xyz','city','tech','food','ocean'].map(s => `https://picsum.photos/seed/${s}/800/600`);
loadURL(urls[Math.floor(Math.random() * urls.length)]);
});
// PASTE
$i('btn-paste').addEventListener('click', async () => {
try {
const items = await navigator.clipboard.read();
for (const item of items) for (const type of item.types) if (type.startsWith('image/')) { loadFile(await item.getType(type)); return; }
toast('No image in clipboard', 'err');
} catch { toast('Paste URL above instead', 'err'); }
});
document.addEventListener('paste', e => {
if (['INPUT','TEXTAREA'].includes(document.activeElement.tagName)) return;
for (const item of e.clipboardData.items) if (item.type.startsWith('image/')) { loadFile(item.getAsFile()); return; }
});
// CANVAS DRAG-DROP
const cw = $i('canvas-wrap');
cw.addEventListener('dragover', e => { e.preventDefault(); cw.classList.add('drag-over'); });
cw.addEventListener('dragleave', () => cw.classList.remove('drag-over'));
cw.addEventListener('drop', e => {
e.preventDefault(); cw.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file?.type.startsWith('image/')) { loadFile(file); return; }
const url = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
if (url?.startsWith('http')) loadURL(url);
});
// TEMPLATES
qsa('.template-item').forEach(t => t.addEventListener('click', () => applyCanvasSize(t.dataset.w, t.dataset.h)));
// SHAPE STYLE LIVE UPDATE
$i('sh-fill').addEventListener('input', function() { if (!$i('sh-no-fill').checked) updateActiveShape('fill', this.value); });
$i('sh-no-fill').addEventListener('change', function() { updateActiveShape('fill', this.checked ? 'transparent' : $i('sh-fill').value); });
$i('sh-stroke').addEventListener('input', function() { updateActiveShape('stroke', this.value); });
$i('sh-stroke-w').addEventListener('change', function() { updateActiveShape('strokeWidth', parseInt(this.value) || 0); });
$i('sh-opacity').addEventListener('input', function() {
$i('sh-op-val').textContent = Math.round(this.value * 100) + '%';
updateActiveShape('opacity', parseFloat(this.value));
});
// TEXT PRESETS
$i('tp-heading').addEventListener('click', () => addText('heading'));
$i('tp-sub').addEventListener('click', () => addText('subheading'));
$i('tp-body').addEventListener('click', () => addText('body'));
$i('tp-caption').addEventListener('click', () => addText('caption'));
$i('btn-add-text').addEventListener('click', () => addText('custom'));
// TEXT LIVE UPDATE
$i('txt-font').addEventListener('change', function() { updateActiveText('fontFamily', this.value); });
$i('txt-size').addEventListener('change', function() { updateActiveText('fontSize', parseInt(this.value)); });
$i('txt-color').addEventListener('input', function() { updateActiveText('fill', this.value); });
$i('txt-stroke-color').addEventListener('input', function() { updateActiveText('stroke', this.value); });
$i('txt-stroke-w').addEventListener('change', function() { updateActiveText('strokeWidth', parseInt(this.value) || 0); });
$i('txt-shadow').addEventListener('change', function() {
updateActiveText('shadow', this.checked ? new fabric.Shadow({ color:'rgba(0,0,0,0.4)', blur:10, offsetX:3, offsetY:3 }) : null);
});
$i('txt-bold').addEventListener('click', function() { boldOn = !boldOn; this.classList.toggle('active', boldOn); updateActiveText('fontWeight', boldOn ? 'bold' : 'normal'); });
$i('txt-italic').addEventListener('click', function() { italicOn = !italicOn; this.classList.toggle('active', italicOn); updateActiveText('fontStyle', italicOn ? 'italic' : 'normal'); });
$i('txt-underline').addEventListener('click', function() { underlineOn = !underlineOn; this.classList.toggle('active', underlineOn); updateActiveText('underline', underlineOn); });
['txt-align-l','txt-align-c','txt-align-r'].forEach(id => {
$i(id).addEventListener('click', function() {
qsa('#sec-text .ir-btn[id^="txt-align"]').forEach(b => b.classList.remove('active'));
this.classList.add('active');
textAlign = { 'txt-align-l':'left','txt-align-c':'center','txt-align-r':'right' }[id];
updateActiveText('textAlign', textAlign);
});
});
$i('txt-opacity').addEventListener('input', function() {
$i('txt-op-val').textContent = Math.round(this.value * 100) + '%';
updateActiveText('opacity', parseFloat(this.value));
});
// BACKGROUND
$i('btn-apply-bg').addEventListener('click', () => { canvas.setBackgroundColor($i('bg-color-pick').value, canvas.renderAll.bind(canvas)); saveHist(); toast('Background updated', 'ok'); });
$i('btn-apply-grad').addEventListener('click', () => applyFabricGradient($i('grad-from').value, $i('grad-to').value, $i('grad-dir').value));
$i('btn-transparent-bg').addEventListener('click', () => { canvas.setBackgroundColor(null, canvas.renderAll.bind(canvas)); saveHist(); toast('Transparent', 'ok'); });
// ADJUSTMENTS
[['adj-brightness','bri-val'],['adj-contrast','con-val'],['adj-saturation','sat-val'],['adj-blur','blur-val']].forEach(([id, vid]) => {
$i(id).addEventListener('input', function() { $i(vid).textContent = parseFloat(this.value).toFixed(1); applyFilters(); });
});
qsa('#filter-chips .ef-chip').forEach(chip => chip.addEventListener('click', function() {
qsa('#filter-chips .ef-chip').forEach(c => c.classList.remove('active'));
this.classList.add('active'); activeFilter = this.dataset.filter; applyFilters();
}));
// OBJECT TRANSFORM
['pos-x','pos-y','size-w','size-h'].forEach(id => $i(id).addEventListener('change', applyPosSize));
$i('obj-rotate').addEventListener('input', function() {
$i('rot-val').textContent = this.value + '°';
const obj = canvas.getActiveObject(); if (obj) { obj.rotate(parseFloat(this.value)); canvas.renderAll(); }
});
$i('obj-rotate').addEventListener('change', saveHist);
$i('obj-opacity').addEventListener('input', function() {
$i('op-val').textContent = Math.round(this.value * 100) + '%';
const obj = canvas.getActiveObject(); if (obj) { obj.set('opacity', parseFloat(this.value)); canvas.renderAll(); }
});
$i('obj-opacity').addEventListener('change', saveHist);
// TRANSFORM BUTTONS
const doFlipH = () => { const o = canvas.getActiveObject(); if (o) { o.set('flipX', !o.flipX); canvas.renderAll(); saveHist(); } };
const doFlipV = () => { const o = canvas.getActiveObject(); if (o) { o.set('flipY', !o.flipY); canvas.renderAll(); saveHist(); } };
const doRotL = () => { const o = canvas.getActiveObject(); if (o) { o.rotate((o.angle||0) - 90); canvas.renderAll(); saveHist(); } };
const doRotR = () => { const o = canvas.getActiveObject(); if (o) { o.rotate((o.angle||0) + 90); canvas.renderAll(); saveHist(); } };
$i('xf-flip-h').onclick = doFlipH; $i('xf-flip-v').onclick = doFlipV;
$i('xf-rot-l').onclick = doRotL; $i('xf-rot-r').onclick = doRotR;
$i('ct-flip-h').onclick = doFlipH; $i('ct-flip-v').onclick = doFlipV;
$i('ct-rot-l').onclick = doRotL; $i('ct-rot-r').onclick = doRotR;
// CROP
$i('ct-crop').onclick = startCrop;
$i('btn-crop-apply').onclick = applyCrop;
$i('btn-crop-cancel').onclick = cancelCrop;
// CANVAS TOOLBAR
$i('ct-bring-f').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.bringForward(o); updateLayers(); saveHist(); } };
$i('ct-send-b').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.sendBackwards(o); updateLayers(); saveHist(); } };
$i('ct-del').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.remove(o); updateLayers(); updateSB(); saveHist(); } };
// LAYERS
$i('lay-up').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.bringForward(o); updateLayers(); saveHist(); } };
$i('lay-dn').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.sendBackwards(o); updateLayers(); saveHist(); } };
$i('lay-del').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.remove(o); updateLayers(); updateSB(); saveHist(); } };
$i('lay-dup').onclick = () => {
const o = canvas.getActiveObject(); if (!o) return;
o.clone(c => { c.set({ left: o.left + 20, top: o.top + 20 }); c._name = (o._name || 'obj') + ' copy'; canvas.add(c); canvas.setActiveObject(c); canvas.renderAll(); saveHist(); });
};
// CANVAS SIZE
$i('btn-canvas-size').onclick = () => { $i('size-modal-overlay').style.display = 'flex'; $i('size-modal-overlay').classList.add('open'); };
$i('size-modal-close').onclick = $i('size-cancel').onclick = () => { $i('size-modal-overlay').style.display = 'none'; };
$i('btn-apply-size').onclick = () => { applyCanvasSize($i('custom-w').value, $i('custom-h').value); $i('size-modal-overlay').style.display = 'none'; };
$i('btn-apply-canvas').onclick = () => applyCanvasSize($i('canvas-w').value, $i('canvas-h').value);
$i('size-modal-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) e.currentTarget.style.display = 'none'; });
// EXPORT
qsa('#right-panel .fmt-card').forEach(card => card.addEventListener('click', function() {
qsa('#right-panel .fmt-card').forEach(c => c.classList.remove('sel')); this.classList.add('sel'); fmt = this.dataset.fmt;
}));
$i('exp-quality').addEventListener('input', function() { exportQuality = parseFloat(this.value); $i('q-val').textContent = Math.round(this.value * 100) + '%'; });
$i('btn-export').onclick = () => doExport({ fmt, quality: exportQuality, scale: exportScale, filename: $i('fname').value });
$i('btn-copy-img').onclick = doCopy;
// EXPORT MODAL
$i('btn-export-open').onclick = () => $i('modal-overlay').classList.add('open');
$i('btn-share').onclick = () => toast('Use Export to download your design', 'ok');
$i('modal-close').onclick = $i('modal-cancel').onclick = () => $i('modal-overlay').classList.remove('open');
$i('modal-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) e.currentTarget.classList.remove('open'); });
qsa('#export-modal .fmt-card').forEach(card => card.addEventListener('click', function() {
qsa('#export-modal .fmt-card').forEach(c => c.classList.remove('sel')); this.classList.add('sel');
}));
$i('modal-quality').addEventListener('input', function() { $i('modal-q-val').textContent = Math.round(this.value * 100) + '%'; });
$i('modal-scale').addEventListener('input', function() { $i('modal-scale-val').textContent = this.value + '×'; exportScale = parseFloat(this.value); });
$i('btn-modal-export').onclick = () => {
const sel = qs('#export-modal .fmt-card.sel');
const f = sel ? sel.dataset.fmt : 'webp';
if (f === 'svg') {
const blob = new Blob([canvas.toSVG()], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = ($i('modal-fname').value || 'design') + '.svg'; a.click();
URL.revokeObjectURL(url); toast('SVG downloaded!', 'ok');
} else {
doExport({ fmt: f, quality: parseFloat($i('modal-quality').value), scale: exportScale, filename: $i('modal-fname').value });
}
$i('modal-overlay').classList.remove('open');
};
// ZOOM
$i('z-in').onclick = () => applyZoom(zoom + 0.15);
$i('z-out').onclick = () => applyZoom(zoom - 0.15);
$i('z-fit').onclick = fitZoom;
$i('zoom-pct').addEventListener('click', () => applyZoom(1));
$i('canvas-wrap').addEventListener('wheel', e => {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault(); applyZoom(zoom + (e.deltaY < 0 ? 0.1 : -0.1));
}, { passive: false });
// CONTEXT MENU
document.addEventListener('contextmenu', e => { if ($i('canvas-container').contains(e.target)) e.preventDefault(); });
document.addEventListener('click', hideCtxMenu);
$i('ctx-bring-f').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.bringForward(o); updateLayers(); saveHist(); } hideCtxMenu(); };
$i('ctx-send-b').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.sendBackwards(o); updateLayers(); saveHist(); } hideCtxMenu(); };
$i('ctx-bring-front').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.bringToFront(o); updateLayers(); saveHist(); } hideCtxMenu(); };
$i('ctx-send-back').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.sendToBack(o); updateLayers(); saveHist(); } hideCtxMenu(); };
$i('ctx-dup').onclick = () => {
const o = canvas.getActiveObject(); if (!o) { hideCtxMenu(); return; }
o.clone(c => { c.set({ left: o.left + 20, top: o.top + 20 }); c._name = (o._name||'obj') + ' copy'; canvas.add(c); canvas.setActiveObject(c); canvas.renderAll(); saveHist(); });
hideCtxMenu();
};
$i('ctx-flip-h').onclick = () => { doFlipH(); hideCtxMenu(); };
$i('ctx-flip-v').onclick = () => { doFlipV(); hideCtxMenu(); };
$i('ctx-del').onclick = () => { const o = canvas.getActiveObject(); if (o) { canvas.remove(o); updateLayers(); updateSB(); saveHist(); } hideCtxMenu(); };
// KEYBOARD SHORTCUTS
document.addEventListener('keydown', e => {
const editing = ['INPUT','TEXTAREA'].includes(document.activeElement.tagName);
if (e.ctrlKey || e.metaKey) {
if (e.key.toLowerCase() === 'z') { e.preventDefault(); undo(); return; }
if (e.key.toLowerCase() === 'y') { e.preventDefault(); redo(); return; }
if (!editing) {
if (e.key.toLowerCase() === 'd') { e.preventDefault(); const o = canvas.getActiveObject(); if (o) o.clone(c => { c.set({ left: o.left+20, top: o.top+20 }); c._name = (o._name||'obj')+' copy'; canvas.add(c); canvas.setActiveObject(c); canvas.renderAll(); saveHist(); }); }
if (e.key.toLowerCase() === 'c') copiedObject = canvas.getActiveObject();
if (e.key.toLowerCase() === 'v' && copiedObject) copiedObject.clone(c => { c.set({ left: copiedObject.left+20, top: copiedObject.top+20 }); c._name = (copiedObject._name||'obj')+' copy'; canvas.add(c); canvas.setActiveObject(c); canvas.renderAll(); saveHist(); });
}
}
if (!editing) {
if ((e.key === 'Delete' || e.key === 'Backspace') && !e.ctrlKey) {
const o = canvas.getActiveObject(); if (o && !o.isEditing) { canvas.remove(o); updateLayers(); updateSB(); saveHist(); }
}
if (e.key === 'Escape' && cropActive) cancelCrop();
const nudge = e.shiftKey ? 10 : 1;
const obj = canvas.getActiveObject();
if (obj && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)) {
e.preventDefault();
if (e.key === 'ArrowLeft') obj.set('left', obj.left - nudge);
if (e.key === 'ArrowRight') obj.set('left', obj.left + nudge);
if (e.key === 'ArrowUp') obj.set('top', obj.top - nudge);
if (e.key === 'ArrowDown') obj.set('top', obj.top + nudge);
canvas.renderAll(); saveHist();
}
}
});
// UNDO/REDO BUTTONS
$i('btn-undo').onclick = undo;
$i('btn-redo').onclick = redo;
// INIT
setTimeout(fitZoom, 120);
setStatus('Ready');
toast('Welcome to DesignForge Pro 🎨', 'ok');
});
</script>
</body>
</html>