Photoshop-Style HTML Editor
A Photoshop-inspired editor UI: menu bar, tool rail, panels and canvas — built with plain HTML/CSS/JS.
Source code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>photoshop codex</title>
</head>
<body>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PhotoShop-Style HTML Editor</title>
<style>
:root{
--bg:#242424;--chrome:#4c4c4c;--chrome2:#3f3f3f;--chrome3:#333;--panel:#3d3d3d;--panel2:#353535;--panel3:#2b2b2b;
--line:#1d1d1d;--soft:#575757;--text:#dedede;--muted:#a8a8a8;--blue:#1473e6;--field:#2f2f2f;--fieldBorder:#686868;
--menu:#efefef;--menuText:#222;--warn:#e0d484;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#202020;color:var(--text);font:12px/1.22 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;overflow:hidden;user-select:none}
button,input,select,textarea{font:inherit;color:inherit}
button{border:0;background:none;padding:0;cursor:default;color:inherit}
input,select,textarea{background:var(--field);border:1px solid var(--fieldBorder);border-radius:2px;color:#e7e7e7;outline:none}
input,select{height:21px;padding:0 5px} textarea{padding:6px;resize:none}
input[type="range"]{padding:0;accent-color:#a0a0a0} input[type="checkbox"]{height:auto;vertical-align:-2px;accent-color:#aaa} input[type="color"]{padding:0;width:28px;height:22px}
.app{height:100vh;display:grid;grid-template-rows:31px 26px 38px 1fr;background:var(--bg)}
.titlebar{display:grid;grid-template-columns:190px 1fr 260px;align-items:center;background:#525252;border-bottom:1px solid #292929;color:#e4e4e4}
.traffic{display:flex;gap:10px;align-items:center;padding-left:16px}.dot{width:13px;height:13px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(0,0,0,.28)}.red{background:#ff5f57}.yellow{background:#febc2e}.green{background:#28c840}
.app-title{text-align:center;font-size:15px;font-weight:650;text-shadow:0 1px 0 #111}.title-actions{display:flex;justify-content:flex-end;align-items:center;gap:10px;padding-right:12px}.share{background:#1473e6;color:white;border-radius:16px;padding:6px 17px;font-weight:650}.title-icon{font-size:17px;width:25px;height:25px;display:grid;place-items:center;border-radius:3px}.title-icon:hover{background:#5e5e5e}
.menubar{display:flex;align-items:center;background:#464646;border-bottom:1px solid #292929;padding-left:6px;z-index:30;white-space:nowrap}.menu-item{position:relative;padding:6px 10px;border-radius:2px;color:#e5e5e5}.menu-item:hover,.menu-item.open{background:#5b5b5b}.dropdown{display:none;position:absolute;top:25px;left:0;min-width:238px;background:var(--menu);color:var(--menuText);border:1px solid #9b9b9b;box-shadow:0 10px 24px rgba(0,0,0,.48);padding:4px 0;z-index:100}.menu-item:hover .dropdown,.menu-item.open .dropdown{display:block}.drop-row{display:grid;grid-template-columns:1fr auto;gap:18px;align-items:center;padding:5px 16px 5px 22px;color:#222}.drop-row:hover{background:#2878d7;color:white}.drop-sep{height:1px;background:#c6c6c6;margin:4px 0}.shortcut{opacity:.75}.workspace-switcher{margin-left:auto;margin-right:8px;display:flex;align-items:center;gap:7px}.workspace-switcher select{height:20px;width:128px;background:#393939;border-color:#222;color:#ddd}
.optionsbar{display:flex;align-items:center;gap:9px;min-width:0;overflow:hidden;white-space:nowrap;background:#3f3f3f;border-top:1px solid #5a5a5a;border-bottom:1px solid #242424;padding:0 9px}.tool-preset{display:flex;align-items:center;gap:8px;padding-right:10px;border-right:1px solid #2c2c2c;min-width:168px}.preset-icon{width:24px;height:24px;border:1px solid #252525;background:#565656;display:grid;place-items:center;border-radius:2px}.opt-group{display:flex;align-items:center;gap:6px;min-width:max-content}.tiny-btn{height:23px;min-width:25px;border:1px solid #252525;background:#4d4d4d;border-radius:2px;display:inline-grid;place-items:center;padding:0 7px;box-shadow:inset 0 1px 0 rgba(255,255,255,.09)}.tiny-btn:hover,.tiny-btn.hot{background:#5f5f5f}.tiny-btn.active{background:#676767;box-shadow:inset 0 0 0 1px #888}.danger:hover{background:#773333}.option-number{width:54px;text-align:right}.long-field{width:92px}.size-preview{width:23px;height:23px;border:1px solid #aaa;border-radius:50%;background:#eee}
.body{min-height:0;display:grid;grid-template-columns:72px minmax(430px,1fr) 374px;background:#282828}.toolbar{display:grid;grid-template-rows:1fr 118px;background:#444;border-right:1px solid #1f1f1f;border-top:1px solid #5b5b5b;padding-top:4px}.tools-grid{display:grid;grid-template-columns:31px 31px;grid-auto-rows:29px;gap:1px;justify-content:center;align-content:start}.tool-btn{position:relative;width:31px;height:29px;display:grid;place-items:center;border-radius:1px;color:#e5e5e5;font-size:16px}.tool-btn:hover{background:#555}.tool-btn.active{background:#2d76d4;color:#fff;box-shadow:inset 0 0 0 1px #75a9ef}.tool-btn .key{position:absolute;left:3px;bottom:1px;font-size:8px;opacity:.68}.tool-btn .corner{position:absolute;right:2px;bottom:2px;border-left:4px solid transparent;border-top:4px solid #d2d2d2;width:0;height:0}.swatches{border-top:1px solid #333;padding-top:8px;display:flex;flex-direction:column;align-items:center;gap:8px}.swatch-wrap{position:relative;width:50px;height:44px}.swatch{position:absolute;width:28px;height:28px;border:1px solid #ddd;box-shadow:0 0 0 1px #111;background:#fff}.swatch.fg{left:2px;top:0;z-index:2}.swatch.bg{right:1px;bottom:1px;background:#000;z-index:1}.swap-colors,.default-colors{position:absolute;font-size:14px;line-height:1}.swap-colors{right:-2px;top:0}.default-colors{left:0;bottom:0}.quick-mode{width:30px;height:22px;border:1px solid #777;background:#555;display:grid;place-items:center}.screen-mode{display:flex;gap:5px}.screen-mode button{width:20px;height:14px;border:1px solid #c9c9c9;opacity:.75}.screen-mode button.active{background:#777}
.center{min-width:0;min-height:0;display:grid;grid-template-rows:32px 1fr 24px;background:#252525}.doc-tabs{display:flex;align-items:flex-end;background:#2b2b2b;border-bottom:1px solid #171717}.doc-tab{height:30px;display:flex;align-items:center;gap:7px;min-width:310px;background:#242424;border-right:1px solid #1e1e1e;border-top:1px solid #555;padding:0 12px;color:#eee}.close-tab{font-size:15px;color:#bbb}.workspace{position:relative;min-width:0;min-height:0;overflow:hidden;background:#262626}.stage-shell{position:absolute;left:0;top:0;transform-origin:0 0;background:#111;box-shadow:0 0 0 1px rgba(0,0,0,.6),0 24px 60px rgba(0,0,0,.5)}#mainCanvas,#overlayCanvas{display:block}#mainCanvas{background:#111}#overlayCanvas{position:absolute;left:0;top:0;cursor:crosshair}.selection-box,.crop-box{position:absolute;display:none;pointer-events:none}.selection-box{border:1px dashed #fff;outline:1px dashed #111;mix-blend-mode:difference}.crop-box{border:1px solid #fafafa;box-shadow:0 0 0 9999px rgba(0,0,0,.45)}.crop-box:before,.crop-box:after{content:"";position:absolute;left:33.33%;top:0;width:33.33%;height:100%;border-left:1px solid rgba(255,255,255,.55);border-right:1px solid rgba(255,255,255,.55)}.crop-box:after{left:0;top:33.33%;width:100%;height:33.33%;border-left:0;border-right:0;border-top:1px solid rgba(255,255,255,.55);border-bottom:1px solid rgba(255,255,255,.55)}.context-task{position:absolute;left:50%;bottom:31px;transform:translateX(-50%);display:flex;gap:8px;align-items:center;background:#3d3d3d;border:1px solid #1a1a1a;border-radius:10px;padding:8px 10px;box-shadow:0 12px 28px rgba(0,0,0,.45);z-index:12}.context-task.hidden{display:none}.doc-status{display:flex;align-items:center;gap:14px;background:#3e3e3e;border-top:1px solid #202020;color:#ddd;padding:0 12px;overflow:hidden}.status-field{background:#505050;border:1px solid #262626;border-radius:2px;height:18px;padding:1px 7px;min-width:62px;text-align:center}.scrollbar-h{position:absolute;height:13px;left:40%;bottom:29px;width:27%;background:#696969;border-radius:8px;opacity:.75}.scrollbar-v{position:absolute;width:13px;right:6px;top:33%;height:37%;background:#696969;border-radius:8px;opacity:.75}
.right{min-height:0;background:#3a3a3a;border-left:1px solid #202020;display:grid;grid-template-rows:282px 1fr}.panel{border-bottom:1px solid #222;background:#3d3d3d;min-height:0;display:flex;flex-direction:column}.tabs-row{height:34px;background:#353535;border-bottom:1px solid #222;display:flex;align-items:flex-end;padding-left:9px;gap:2px}.panel-tab{height:30px;padding:8px 11px 0;background:#353535;color:#c3c3c3;font-weight:650;border-left:1px solid transparent;border-right:1px solid transparent}.panel-tab.active{background:#424242;color:#f1f1f1;border-color:#2b2b2b}.panel-menu{margin-left:auto;margin-right:9px;font-size:18px;color:#bbb;align-self:center}.panel-content{padding:9px 12px;min-height:0;overflow:auto;flex:1}.hist-wrap{height:111px;background:#555;border:1px solid #2a2a2a;position:relative;display:grid;place-items:center}#histCanvas{background:#474747}.hist-warning{position:absolute;right:9px;top:7px;color:#ddd}.adjust-title{font-size:13px;margin:9px 0 7px 1px}.adjust-grid{display:grid;grid-template-columns:repeat(7,24px);gap:8px}.adjust-icon{width:22px;height:18px;border:1px solid #cfcfcf;background:#d8d8d8;position:relative;border-radius:1px}.adjust-icon:hover,.adjust-icon.active{box-shadow:0 0 0 1px #fff}.adjust-icon.sun{border-radius:50%;background:transparent;border:1px dashed #ddd}.adjust-icon.curve:after{content:"";position:absolute;left:2px;right:2px;bottom:3px;height:8px;border-bottom:2px solid #666;border-radius:50%}.adjust-icon.levels{background:linear-gradient(90deg,#ddd 0 28%,#aaa 28% 70%,#666 70%)}.adjust-icon.gradient{background:linear-gradient(90deg,#f0f0f0,#555)}.adjust-icon.poster{background:linear-gradient(90deg,#ddd 0 33%,#888 33% 66%,#333 66%)}.info-grid{display:grid;grid-template-columns:auto 1fr;gap:6px 10px}.properties-panel{display:grid;gap:7px;margin-top:8px}.slider-line{display:grid;grid-template-columns:82px 1fr 52px;gap:7px;align-items:center}.slider-line input[type=number]{width:52px;text-align:right}.mini-list{display:grid;gap:5px}.mini-row{display:grid;grid-template-columns:24px 1fr auto;align-items:center;gap:8px;padding:5px;border:1px solid transparent}.mini-row:hover{background:#4b4b4b}.mini-row.active{background:#5c5c5c;border-color:#6b6b6b}.nav-holder{display:grid;place-items:center;height:170px}#navCanvas{background:#222;border:1px solid #222}
.layers-panel{min-height:0;display:grid;grid-template-rows:34px 32px 34px 1fr 31px;background:#3b3b3b}.layer-filter{display:flex;align-items:center;gap:6px;padding:5px 8px;border-bottom:1px solid #2b2b2b;background:#363636}.layer-filter input{height:21px;width:126px;background:#444;border-color:#292929;color:#ddd}.layer-controls{display:flex;align-items:center;gap:7px;padding:5px 8px;border-bottom:1px solid #2a2a2a;background:#3c3c3c}.layer-controls select{height:22px;width:120px;background:#474747;border-color:#2a2a2a}.opacity-box{display:flex;align-items:center;gap:4px;margin-left:auto}.opacity-box input{width:46px;text-align:right}.layer-content{overflow:auto;background:#3a3a3a;padding:4px 7px}.layer-row{display:grid;grid-template-columns:23px 42px 1fr 26px;gap:7px;align-items:center;min-height:42px;padding:4px 5px;border:1px solid transparent}.layer-row.active{background:#5c5c5c;border-color:#6a6a6a}.layer-row:hover{background:#4b4b4b}.eye{text-align:center;font-size:15px}.thumb{width:38px;height:27px;background:#202020;border:1px solid #222;object-fit:cover}.layer-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lock{text-align:center}.layer-footer{display:flex;align-items:center;justify-content:flex-end;gap:11px;padding:0 10px;border-top:1px solid #282828;background:#393939;font-size:17px}.footer-icon:hover{color:#fff}.muted{color:var(--muted)}.kbd{display:inline-block;min-width:20px;padding:2px 5px;background:#111;color:#eee;border-radius:3px;border:1px solid #777;box-shadow:inset 0 -1px 0 rgba(255,255,255,.16);font-size:11px;text-align:center;margin:0 1px}.modal-backdrop{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.42);z-index:300}.modal{width:min(880px,91vw);max-height:84vh;background:#ececec;color:#222;border:1px solid #111;box-shadow:0 22px 72px rgba(0,0,0,.65);border-radius:3px;display:grid;grid-template-rows:38px 1fr 44px;overflow:hidden}.modal-title{background:#d8d8d8;border-bottom:1px solid #aaa;display:flex;align-items:center;justify-content:space-between;padding:0 13px;font-weight:700}.modal-body{padding:14px 16px;overflow:auto;user-select:text}.modal-body table{width:100%;border-collapse:collapse;font-size:12px}.modal-body th,.modal-body td{border-bottom:1px solid #c8c8c8;text-align:left;padding:6px 8px}.modal-body th{background:#f7f7f7;position:sticky;top:0}.modal-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;background:#ddd;border-top:1px solid #aaa;padding:7px 12px}.modal-actions button{height:28px;border:1px solid #777;background:#f8f8f8;border-radius:3px;padding:0 16px;color:#111}.modal-actions button.primary{background:#1473e6;color:#fff;border-color:#0b55ae}.toast{position:fixed;left:50%;bottom:38px;transform:translateX(-50%);background:#111;color:#fff;border:1px solid #555;border-radius:5px;padding:9px 14px;box-shadow:0 10px 30px rgba(0,0,0,.45);opacity:0;pointer-events:none;transition:opacity .16s ease;z-index:400;max-width:72vw;text-align:center}.toast.show{opacity:1}.sr-only{position:absolute;left:-9999px}.hidden{display:none!important}.checkered{background-color:#bfbfbf;background-image:linear-gradient(45deg,#9a9a9a 25%,transparent 25%),linear-gradient(-45deg,#9a9a9a 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#9a9a9a 75%),linear-gradient(-45deg,transparent 75%,#9a9a9a 75%);background-size:20px 20px;background-position:0 0,0 10px,10px -10px,-10px 0}
@media(max-width:980px){.body{grid-template-columns:68px 1fr 315px}.right{grid-template-rows:258px 1fr}.titlebar{grid-template-columns:150px 1fr 190px}.menu-item{padding:6px 7px}.workspace-switcher{display:none}}
</style>
</head>
<body>
<div class="app" id="app">
<div class="titlebar">
<div class="traffic"><span class="dot red"></span><span class="dot yellow"></span><span class="dot green"></span></div>
<div class="app-title">Adobe Photoshop 2023</div>
<div class="title-actions"><button class="share" data-action="share">Share</button><button class="title-icon" data-action="commandPalette" title="Search">⌕</button><button class="title-icon" data-action="togglePanels" title="Panels">▣</button><button class="title-icon" data-action="workspaceMenu" title="Workspace">⌄</button></div>
</div>
<div class="menubar" id="menubar">
<div class="menu-item" data-menu="file">File<div class="dropdown">
<div class="drop-row" data-action="newDoc"><span>New...</span><span class="shortcut">⌘N</span></div><div class="drop-row" data-action="open"><span>Open...</span><span class="shortcut">⌘O</span></div><div class="drop-row" data-action="openSample"><span>Open Sample Image</span></div><div class="drop-row" data-action="duplicateDoc"><span>Duplicate Document</span></div><div class="drop-sep"></div><div class="drop-row" data-action="save"><span>Save</span><span class="shortcut">⌘S</span></div><div class="drop-row" data-action="exportPng"><span>Export PNG...</span><span class="shortcut">⌥⇧⌘W</span></div><div class="drop-row" data-action="exportJpg"><span>Export JPG...</span></div><div class="drop-sep"></div><div class="drop-row" data-action="closeDoc"><span>Close</span><span class="shortcut">⌘W</span></div>
</div></div>
<div class="menu-item" data-menu="edit">Edit<div class="dropdown">
<div class="drop-row" data-action="undo"><span>Undo</span><span class="shortcut">⌘Z</span></div><div class="drop-row" data-action="redo"><span>Redo</span><span class="shortcut">⇧⌘Z</span></div><div class="drop-sep"></div><div class="drop-row" data-action="copy"><span>Copy</span><span class="shortcut">⌘C</span></div><div class="drop-row" data-action="cut"><span>Cut</span><span class="shortcut">⌘X</span></div><div class="drop-row" data-action="paste"><span>Paste</span><span class="shortcut">⌘V</span></div><div class="drop-row" data-action="fill"><span>Fill...</span><span class="shortcut">⇧F5</span></div><div class="drop-row" data-action="stroke"><span>Stroke...</span></div><div class="drop-row" data-action="contentAwareFill"><span>Content-Aware Fill</span></div><div class="drop-sep"></div><div class="drop-row" data-action="freeTransform"><span>Free Transform</span><span class="shortcut">⌘T</span></div><div class="drop-row" data-action="preferences"><span>Preferences...</span></div><div class="drop-row" data-action="shortcuts"><span>Keyboard Shortcuts...</span><span class="shortcut">⌥⇧⌘K</span></div>
</div></div>
<div class="menu-item" data-menu="image">Image<div class="dropdown">
<div class="drop-row" data-action="autoTone"><span>Auto Tone</span><span class="shortcut">⇧⌘L</span></div><div class="drop-row" data-action="autoContrast"><span>Auto Contrast</span></div><div class="drop-row" data-action="autoColor"><span>Auto Color</span></div><div class="drop-sep"></div><div class="drop-row" data-action="imageSize"><span>Image Size...</span></div><div class="drop-row" data-action="canvasSize"><span>Canvas Size...</span></div><div class="drop-row" data-action="cropToSelection"><span>Crop</span></div><div class="drop-row" data-action="rotateCanvas"><span>Image Rotation 90°</span></div><div class="drop-sep"></div><div class="drop-row" data-action="invert"><span>Invert</span><span class="shortcut">⌘I</span></div><div class="drop-row" data-action="desaturate"><span>Desaturate</span><span class="shortcut">⇧⌘U</span></div>
</div></div>
<div class="menu-item" data-menu="layer">Layer<div class="dropdown">
<div class="drop-row" data-action="newLayer"><span>New Layer...</span><span class="shortcut">⇧⌘N</span></div><div class="drop-row" data-action="duplicateLayer"><span>Duplicate Layer</span><span class="shortcut">⌘J</span></div><div class="drop-row" data-action="deleteLayer"><span>Delete Layer</span></div><div class="drop-row" data-action="newGroup"><span>New Group</span></div><div class="drop-sep"></div><div class="drop-row" data-action="addMask"><span>Layer Mask</span></div><div class="drop-row" data-action="fx"><span>Layer Style...</span></div><div class="drop-row" data-action="mergeVisible"><span>Merge Visible</span><span class="shortcut">⇧⌘E</span></div><div class="drop-row" data-action="flatten"><span>Flatten Image</span></div>
</div></div>
<div class="menu-item" data-menu="type">Type<div class="dropdown">
<div class="drop-row" data-tool="type"><span>Horizontal Type Tool</span><span class="shortcut">T</span></div><div class="drop-row" data-tool="verticalType"><span>Vertical Type Tool</span></div><div class="drop-row" data-action="lorem"><span>Paste Lorem Ipsum</span></div><div class="drop-row" data-action="fontBigger"><span>Bigger Text</span></div><div class="drop-row" data-action="fontSmaller"><span>Smaller Text</span></div>
</div></div>
<div class="menu-item" data-menu="select">Select<div class="dropdown">
<div class="drop-row" data-action="selectAll"><span>All</span><span class="shortcut">⌘A</span></div><div class="drop-row" data-action="deselect"><span>Deselect</span><span class="shortcut">⌘D</span></div><div class="drop-row" data-action="reselect"><span>Reselect</span><span class="shortcut">⇧⌘D</span></div><div class="drop-row" data-action="invertSelection"><span>Inverse</span><span class="shortcut">⇧⌘I</span></div><div class="drop-sep"></div><div class="drop-row" data-action="selectSubject"><span>Subject</span></div><div class="drop-row" data-action="selectAndMask"><span>Select and Mask...</span></div>
</div></div>
<div class="menu-item" data-menu="filter">Filter<div class="dropdown">
<div class="drop-row" data-action="blurFilter"><span>Blur</span></div><div class="drop-row" data-action="gaussianBlur"><span>Gaussian Blur...</span></div><div class="drop-row" data-action="sharpenFilter"><span>Sharpen</span></div><div class="drop-row" data-action="noiseFilter"><span>Add Noise</span></div><div class="drop-row" data-action="medianFilter"><span>Median</span></div><div class="drop-row" data-action="pixelateFilter"><span>Pixelate</span></div>
</div></div>
<div class="menu-item" data-menu="3d">3D<div class="dropdown">
<div class="drop-row" data-action="new3DLayer"><span>New 3D Extrusion from Layer</span></div><div class="drop-row" data-action="rotate3D"><span>Rotate 3D View</span></div><div class="drop-row" data-action="lighting3D"><span>Environment Lighting...</span></div>
</div></div>
<div class="menu-item" data-menu="view">View<div class="dropdown">
<div class="drop-row" data-action="zoomIn"><span>Zoom In</span><span class="shortcut">⌘+</span></div><div class="drop-row" data-action="zoomOut"><span>Zoom Out</span><span class="shortcut">⌘−</span></div><div class="drop-row" data-action="fit"><span>Fit on Screen</span><span class="shortcut">⌘0</span></div><div class="drop-row" data-action="actualPixels"><span>100%</span><span class="shortcut">⌘1</span></div><div class="drop-sep"></div><div class="drop-row" data-action="toggleRulers"><span>Rulers</span><span class="shortcut">⌘R</span></div><div class="drop-row" data-action="toggleGuides"><span>Extras / Guides</span><span class="shortcut">⌘H</span></div><div class="drop-row" data-action="toggleGrid"><span>Grid</span></div><div class="drop-row" data-action="toggleSnap"><span>Snap</span></div><div class="drop-row" data-action="screenMode"><span>Screen Mode</span><span class="shortcut">F</span></div>
</div></div>
<div class="menu-item" data-menu="window">Window<div class="dropdown">
<div class="drop-row" data-action="panelHistogram"><span>Histogram</span></div><div class="drop-row" data-action="panelNavigator"><span>Navigator</span></div><div class="drop-row" data-action="panelInfo"><span>Info</span></div><div class="drop-row" data-action="panelLayers"><span>Layers</span><span class="shortcut">F7</span></div><div class="drop-row" data-action="panelChannels"><span>Channels</span></div><div class="drop-row" data-action="panelPaths"><span>Paths</span></div><div class="drop-row" data-action="workspaceReset"><span>Workspace Reset</span></div>
</div></div>
<div class="menu-item" data-menu="help">Help<div class="dropdown">
<div class="drop-row" data-action="shortcuts"><span>Keyboard Shortcuts</span></div><div class="drop-row" data-action="runAudit"><span>Run UI Button Check</span></div><div class="drop-row" data-action="about"><span>About This Build</span></div>
</div></div>
<div class="workspace-switcher"><span>Workspace</span><select id="workspaceSelect" data-select-action="workspace"><option>Photography</option><option>Essentials</option><option>Painting</option><option>Design</option><option>3D</option></select></div>
</div>
<div class="optionsbar" id="optionsbar"></div>
<div class="body" id="bodyArea">
<aside class="toolbar">
<div class="tools-grid" id="toolbarTools"></div>
<div class="swatches">
<div class="swatch-wrap" title="Foreground / Background colors">
<button class="swap-colors" data-action="swapColors" title="Swap colors">↔</button><button class="default-colors" data-action="defaultColors" title="Default colors">◱</button>
<input class="swatch fg" id="fgColor" type="color" value="#ffffff" aria-label="Foreground color"><input class="swatch bg" id="bgColor" type="color" value="#000000" aria-label="Background color">
</div>
<button class="quick-mode" data-action="quickMask" title="Quick Mask">◌</button>
<div class="screen-mode"><button data-action="standardScreen" class="active" title="Standard screen"></button><button data-action="fullScreen" title="Full screen"></button></div>
</div>
</aside>
<main class="center">
<div class="doc-tabs"><div class="doc-tab active"><button class="close-tab" data-action="closeDoc" title="Close document">×</button><strong id="docName">DSC00579.RAW</strong><span>@</span><span id="tabZoom">33.3%</span><span id="docMode">(RGB/16*)</span></div></div>
<div class="workspace" id="workspace">
<div class="stage-shell" id="stageShell"><canvas id="mainCanvas" width="1200" height="675"></canvas><canvas id="overlayCanvas" width="1200" height="675"></canvas><div id="selectionBox" class="selection-box"></div><div id="cropBox" class="crop-box"></div></div>
<div class="scrollbar-v"></div><div class="scrollbar-h"></div>
<div class="context-task" id="contextTask"><span id="contextText">Move tool</span><button class="tiny-btn" data-action="applyContext">Apply</button><button class="tiny-btn" data-action="cancelContext">Cancel</button></div>
</div>
<div class="doc-status"><span class="status-field" id="statusZoom">33.3%</span><span id="docSize">6000 px × 3375 px (240 ppi)</span><span id="statusHint">Ready</span></div>
</main>
<aside class="right" id="rightPanels">
<section class="panel">
<div class="tabs-row"><button class="panel-tab active" data-action="panelHistogram" data-top-tab="histogram">Histogram</button><button class="panel-tab" data-action="panelNavigator" data-top-tab="navigator">Navigator</button><button class="panel-tab" data-action="panelInfo" data-top-tab="info">Info</button><button class="panel-menu" data-action="panelMenu">≡</button></div>
<div class="panel-content" id="topPanelContent"></div>
</section>
<section class="layers-panel">
<div class="tabs-row"><button class="panel-tab active" data-action="panelLayers" data-bottom-tab="layers">Layers</button><button class="panel-tab" data-action="panelChannels" data-bottom-tab="channels">Channels</button><button class="panel-tab" data-action="panelPaths" data-bottom-tab="paths">Paths</button><button class="panel-menu" data-action="layerPanelMenu">≡</button></div>
<div class="layer-filter"><input id="layerFilter" placeholder="Kind" aria-label="Filter layers"><button class="tiny-btn" data-action="filterPixels" title="Pixel layers">▧</button><button class="tiny-btn" data-action="filterAdjustments" title="Adjustment layers">◐</button><button class="tiny-btn" data-action="filterText" title="Text layers">T</button><button class="tiny-btn" data-action="filterShape" title="Shape layers">▱</button><button class="tiny-btn" data-action="filterClear" title="Clear filter">✕</button></div>
<div class="layer-controls"><select id="blendMode" data-select-action="blend"><option value="normal">Normal</option><option value="multiply">Multiply</option><option value="screen">Screen</option><option value="overlay">Overlay</option><option value="darken">Darken</option><option value="lighten">Lighten</option><option value="color-dodge">Color Dodge</option><option value="color-burn">Color Burn</option></select><div class="opacity-box"><span>Opacity</span><input id="layerOpacity" type="number" min="0" max="100" value="100" data-input-action="opacity"><span>%</span><span>Fill</span><input id="layerFill" type="number" min="0" max="100" value="100" data-input-action="fillOpacity"><span>%</span></div></div>
<div class="layer-content" id="bottomPanelContent"></div>
<div class="layer-footer"><button class="footer-icon" data-action="linkLayer" title="Link">∞</button><button class="footer-icon" data-action="fx" title="fx">fx</button><button class="footer-icon" data-action="addMask" title="Add mask">▣</button><button class="footer-icon" data-action="adjustLayer" title="Adjustment layer">◐</button><button class="footer-icon" data-action="newGroup" title="New group">▰</button><button class="footer-icon" data-action="newLayer" title="New layer">+</button><button class="footer-icon danger" data-action="deleteLayer" title="Delete layer">♲</button></div>
</section>
</aside>
</div>
</div>
<input id="fileInput" class="sr-only" type="file" accept="image/*">
<div class="toast" id="toast"></div>
<div class="modal-backdrop" id="modalBackdrop" role="dialog" aria-modal="true"><div class="modal"><div class="modal-title"><span id="modalTitle">Dialog</span><button data-action="closeModal">×</button></div><div class="modal-body" id="modalBody"></div><div class="modal-actions" id="modalActions"><button data-action="closeModal">Cancel</button><button class="primary" data-action="closeModal">OK</button></div></div></div>
<script>
(() => {
'use strict';
const $=(s,r=document)=>r.querySelector(s); const $$=(s,r=document)=>Array.from(r.querySelectorAll(s));
const mainCanvas=$('#mainCanvas'), overlayCanvas=$('#overlayCanvas'), workspace=$('#workspace'), stageShell=$('#stageShell');
const ctx=mainCanvas.getContext('2d',{willReadFrequently:true}); const octx=overlayCanvas.getContext('2d');
const selectionBox=$('#selectionBox'), cropBox=$('#cropBox');
const toolGroups=[
{key:'V',tools:[['move','Move Tool','↕'],['artboard','Artboard Tool','▦']]},
{key:'M',tools:[['marquee','Rectangular Marquee Tool','▭'],['ellipticalMarquee','Elliptical Marquee Tool','◯'],['singleRow','Single Row Marquee Tool','━'],['singleColumn','Single Column Marquee Tool','┃']]},
{key:'L',tools:[['lasso','Lasso Tool','⌁'],['polygonalLasso','Polygonal Lasso Tool','⬠'],['magneticLasso','Magnetic Lasso Tool','⌁']]},
{key:'W',tools:[['objectSelection','Object Selection Tool','▢'],['quickSelection','Quick Selection Tool','✣'],['magicWand','Magic Wand Tool','✦']]},
{key:'C',tools:[['crop','Crop Tool','⌗'],['perspectiveCrop','Perspective Crop Tool','◫'],['slice','Slice Tool','⊞'],['sliceSelect','Slice Select Tool','▣']]},
{key:'I',tools:[['eyedropper','Eyedropper Tool','i'],['colorSampler','Color Sampler Tool','⊙'],['ruler','Ruler Tool','╱'],['note','Note Tool','◫'],['count','Count Tool','#']]},
{key:'J',tools:[['spotHeal','Spot Healing Brush Tool','✚'],['healingBrush','Healing Brush Tool','✚'],['patch','Patch Tool','▱'],['contentMove','Content-Aware Move Tool','↗'],['redEye','Red Eye Tool','◉']]},
{key:'B',tools:[['brush','Brush Tool','●'],['pencil','Pencil Tool','✎'],['colorReplacement','Color Replacement Tool','◒'],['mixerBrush','Mixer Brush Tool','◍']]},
{key:'S',tools:[['cloneStamp','Clone Stamp Tool','♟'],['patternStamp','Pattern Stamp Tool','▥']]},
{key:'Y',tools:[['historyBrush','History Brush Tool','↶'],['artHistoryBrush','Art History Brush Tool','✺']]},
{key:'E',tools:[['eraser','Eraser Tool','▱'],['backgroundEraser','Background Eraser Tool','◌'],['magicEraser','Magic Eraser Tool','✦']]},
{key:'G',tools:[['gradient','Gradient Tool','▤'],['paintBucket','Paint Bucket Tool','▾'],['materialDrop','Material Drop Tool','◩']]},
{key:'R',tools:[['blur','Blur Tool','●'],['sharpen','Sharpen Tool','▲'],['smudge','Smudge Tool','☝']]},
{key:'O',tools:[['dodge','Dodge Tool','◐'],['burn','Burn Tool','●'],['sponge','Sponge Tool','◍']]},
{key:'P',tools:[['pen','Pen Tool','✒'],['freeformPen','Freeform Pen Tool','〰'],['curvaturePen','Curvature Pen Tool','⌒'],['addAnchor','Add Anchor Point Tool','+'],['deleteAnchor','Delete Anchor Point Tool','−']]},
{key:'T',tools:[['type','Horizontal Type Tool','T'],['verticalType','Vertical Type Tool','T↕'],['typeMask','Type Mask Tool','T□']]},
{key:'A',tools:[['pathSelection','Path Selection Tool','▸'],['directSelection','Direct Selection Tool','▹']]},
{key:'U',tools:[['rectangle','Rectangle Tool','▭'],['ellipse','Ellipse Tool','◯'],['triangle','Triangle Tool','△'],['polygon','Polygon Tool','⬟'],['line','Line Tool','╲'],['customShape','Custom Shape Tool','★']]},
{key:'H',tools:[['hand','Hand Tool','✋'],['rotateView','Rotate View Tool','⟳']]},
{key:'Z',tools:[['zoom','Zoom Tool','⌕']]}
];
const toolMap=new Map(), keyToGroup=new Map();
toolGroups.forEach(g=>{keyToGroup.set(g.key.toLowerCase(),g);g.tools.forEach((t,i)=>toolMap.set(t[0],{id:t[0],label:t[1],icon:t[2],key:g.key,group:g,index:i}));});
const brushTools=new Set(['brush','pencil','eraser','backgroundEraser','magicEraser','spotHeal','healingBrush','cloneStamp','patternStamp','historyBrush','artHistoryBrush','blur','sharpen','smudge','dodge','burn','sponge','colorReplacement','mixerBrush']);
const selectionTools=new Set(['marquee','ellipticalMarquee','singleRow','singleColumn','lasso','polygonalLasso','magneticLasso','objectSelection','quickSelection','magicWand']);
const cropSliceTools=new Set(['crop','perspectiveCrop','slice','sliceSelect']);
const shapeTools=new Set(['rectangle','ellipse','triangle','polygon','line','customShape']);
const pathTools=new Set(['pen','freeformPen','curvaturePen','addAnchor','deleteAnchor','pathSelection','directSelection']);
const state={
docName:'DSC00579.RAW',nativeW:6000,nativeH:3375,ppi:240,activeTool:'move',tempTool:null,zoom:.333,panX:0,panY:0,rotation:0,
fg:'#ffffff',bg:'#000000',brushSize:38,hardness:70,opacity:100,flow:100,feather:0,tolerance:32,selectionMode:'new',gradientKind:'linear',shapeMode:'fill',textAlign:'left',fontFamily:'Arial',fontSize:54,
adjustments:{brightness:0,contrast:0,saturation:100,hue:0,exposure:0,gamma:1,temperature:0},layers:[],activeLayer:0,layerFilter:'',selection:null,previousSelection:null,crop:null,guides:true,grid:false,rulers:false,snap:false,quickMask:false,screenMode:'standard',
slices:[],activeSlice:-1,markers:[],notes:[],measure:null,paths:[],activePath:-1,count:0,artboards:[],cloneSource:null,clipboard:null,initialComposite:null,lastComposite:null,history:[],redo:[],drawing:false,start:null,last:null,points:[],needsHist:true,topPanel:'histogram',bottomPanel:'layers'
};
function createLayer(name,opts={}){const c=document.createElement('canvas');c.width=mainCanvas.width;c.height=mainCanvas.height;const l={id:'lyr-'+Math.random().toString(36).slice(2),name,canvas:c,visible:opts.visible!==false,locked:!!opts.locked,opacity:opts.opacity??1,fillOpacity:opts.fillOpacity??1,blend:opts.blend||'normal',offsetX:opts.offsetX||0,offsetY:opts.offsetY||0,kind:opts.kind||'pixel',linked:!!opts.linked,effects:{...(opts.effects||{})},mask:opts.mask||null,collapsed:false}; if(opts.draw)opts.draw(c.getContext('2d')); return l;}
function init(){buildToolbar();bindEvents();openSample(false);renderOptions();showProperties('exposure');toast('Rebuilt: every visible control has a handler. Use Help > Run UI Button Check.');}
function drawSample(g){const w=mainCanvas.width,h=mainCanvas.height;const sky=g.createLinearGradient(0,0,0,h);sky.addColorStop(0,'#030711');sky.addColorStop(.35,'#09172a');sky.addColorStop(.66,'#2b2d38');sky.addColorStop(.78,'#8d745c');sky.addColorStop(1,'#11140f');g.fillStyle=sky;g.fillRect(0,0,w,h);let seed=73;const rnd=()=>{seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;};g.save();g.globalAlpha=.35;g.translate(w*.46,h*.2);g.rotate(-.26);for(let i=0;i<10;i++){const grad=g.createRadialGradient(0,0,20,0,0,Math.max(w,h)*(.38+i*.035));grad.addColorStop(0,'rgba(255,235,210,.16)');grad.addColorStop(.35,'rgba(145,112,160,.07)');grad.addColorStop(1,'rgba(0,0,0,0)');g.fillStyle=grad;g.fillRect(-w,-h,w*2,h*2);}g.restore();for(let i=0;i<1900;i++){const x=rnd()*w,y=rnd()*h*.76,a=.22+rnd()*.78,r=rnd()>.985?1.8+rnd()*1.5:.35+rnd()*1.05,t=rnd();g.fillStyle=t>.86?`rgba(255,210,160,${a})`:t<.12?`rgba(175,200,255,${a})`:`rgba(255,255,245,${a})`;g.beginPath();g.arc(x,y,r,0,Math.PI*2);g.fill();}const glow=g.createRadialGradient(w*.47,h*.76,20,w*.47,h*.76,w*.57);glow.addColorStop(0,'rgba(255,195,115,.45)');glow.addColorStop(.35,'rgba(255,175,120,.2)');glow.addColorStop(1,'rgba(0,0,0,0)');g.fillStyle=glow;g.fillRect(0,0,w,h);g.fillStyle='#141a18';g.beginPath();g.moveTo(0,h*.76);for(let x=0;x<=w;x+=60){g.lineTo(x,h*.76+Math.sin(x*.018)*18+Math.sin(x*.006)*25);}g.lineTo(w,h);g.lineTo(0,h);g.closePath();g.fill();for(let pass=0;pass<3;pass++){g.fillStyle=pass===0?'#0a0d09':pass===1?'#111609':'#18200d';g.beginPath();g.moveTo(0,h);for(let x=-40;x<=w+40;x+=18){g.lineTo(x,h*(.83+pass*.025)+Math.sin((x+pass*31)*.03)*22);}g.lineTo(w,h);g.lineTo(0,h);g.closePath();g.fill();}g.strokeStyle='rgba(28,37,16,.8)';g.lineWidth=2;for(let i=0;i<240;i++){const x=rnd()*w,base=h*(.84+rnd()*.16),top=base-(25+rnd()*90);g.beginPath();g.moveTo(x,base);g.quadraticCurveTo(x+(rnd()-.5)*18,(base+top)/2,x+(rnd()-.5)*28,top);g.stroke();}}
function openSample(show=true){mainCanvas.width=overlayCanvas.width=1200;mainCanvas.height=overlayCanvas.height=675;state.docName='DSC00579.RAW';state.nativeW=6000;state.nativeH=3375;state.ppi=240;state.layers=[createLayer('Background',{locked:true,draw:drawSample})];state.activeLayer=0;state.selection=state.previousSelection=state.crop=null;state.slices=[];state.markers=[];state.notes=[];state.paths=[];state.artboards=[];state.count=0;state.adjustments={brightness:0,contrast:0,saturation:100,hue:0,exposure:0,gamma:1,temperature:0};state.history=[];state.redo=[];$('#docName').textContent=state.docName;fitToScreen(false);renderAll();state.initialComposite=cloneCanvas(mainCanvas);if(show)toast('Sample image loaded.');}
function buildToolbar(){const holder=$('#toolbarTools');holder.innerHTML='';toolGroups.forEach(group=>{const t=group.tools[0];const b=document.createElement('button');b.className='tool-btn';b.dataset.tool=t[0];b.dataset.rootTool=t[0];b.title=`${t[1]} (${group.key})`;b.innerHTML=`<span>${t[2]}</span><span class="key">${group.key}</span>${group.tools.length>1?'<span class="corner"></span>':''}`;holder.appendChild(b);});updateToolbar();}
function bindEvents(){document.addEventListener('click',onDocClick);document.addEventListener('change',onDocChange);document.addEventListener('input',onDocInput);$('#toolbarTools').addEventListener('click',e=>{const b=e.target.closest('.tool-btn');if(!b)return;e.stopPropagation();const info=toolMap.get(b.dataset.tool);if(state.activeTool===b.dataset.tool && info.group.tools.length>1)cycleToolGroup(info.group);else setTool(b.dataset.tool);});$('#toolbarTools').addEventListener('contextmenu',e=>{const b=e.target.closest('.tool-btn');if(!b)return;e.preventDefault();openToolFlyout(toolMap.get(b.dataset.tool).group);});$('#fgColor').addEventListener('input',e=>{state.fg=e.target.value;renderOptions();});$('#bgColor').addEventListener('input',e=>{state.bg=e.target.value;renderOptions();});$('#fileInput').addEventListener('change',onOpenFile);overlayCanvas.addEventListener('pointerdown',pointerDown);overlayCanvas.addEventListener('pointermove',pointerMove);window.addEventListener('pointerup',pointerUp);overlayCanvas.addEventListener('dblclick',()=>{if(state.crop)applyCrop();});workspace.addEventListener('wheel',onWheel,{passive:false});window.addEventListener('resize',()=>fitToScreen(false));document.addEventListener('keydown',onKeyDown);document.addEventListener('keyup',onKeyUp);}
function onDocClick(e){const menu=e.target.closest('.menu-item[data-menu]');if(menu && !e.target.closest('.drop-row')){e.preventDefault();$$('.menu-item.open').forEach(m=>m!==menu&&m.classList.remove('open'));menu.classList.toggle('open');return;} if(!e.target.closest('.menu-item')) $$('.menu-item.open').forEach(m=>m.classList.remove('open'));
const toolEl=e.target.closest('[data-tool]');if(toolEl && !toolEl.closest('#toolbarTools')){setTool(toolEl.dataset.tool);e.preventDefault();return;}
const action=e.target.closest('[data-action]')?.dataset.action;if(action){e.preventDefault();runAction(action,e);return;}
const adj=e.target.closest('[data-adjust]')?.dataset.adjust;if(adj){showProperties(adj);return;}
const layerRow=e.target.closest('[data-layer-index]');if(layerRow){state.activeLayer=+layerRow.dataset.layerIndex;renderBottomPanel();renderLayerControls();return;}}
function onDocChange(e){const select=e.target.closest('[data-select-action]');if(!select)return;runSelectAction(select.dataset.selectAction,select.value);}
function onDocInput(e){const inp=e.target.closest('[data-input-action]');if(!inp)return;runInputAction(inp.dataset.inputAction,inp.value,e);}
function runAction(action,e){closeOpenMenus();const fn=actions[action];if(fn)fn(e);else toast(`${action} connected.`);}
function closeOpenMenus(){$$('.menu-item.open').forEach(m=>m.classList.remove('open'));}
const actions={
newDoc,open:()=>$('#fileInput').click(),openSample:()=>openSample(true),duplicateDoc,save:()=>exportImage('png'),exportPng:()=>exportImage('png'),exportJpg:()=>exportImage('jpeg'),closeDoc,
undo,redo,copy:copySelection,cut:cutSelection,paste:pasteClipboard,fill:()=>fillSelection(),stroke:()=>strokeSelection(),contentAwareFill,freeTransform:()=>{setTool('move');drawTransformHandles();toast('Free Transform handles shown. Drag with the Move tool.');},preferences,shortcuts:showShortcuts,
autoTone,autoContrast,autoColor,imageSize,canvasSize,cropToSelection,rotateCanvas,invert:()=>applyPixelFilter('invert'),desaturate:()=>applyPixelFilter('desaturate'),
newLayer:()=>addNewLayer(),duplicateLayer,deleteLayer,newGroup,addMask,fx,mergeVisible,flatten,lorem:()=>addTextAt(mainCanvas.width/2,mainCanvas.height/2,'Lorem ipsum dolor sit amet'),fontBigger:()=>{state.fontSize=clamp(state.fontSize+6,6,300);renderOptions();toast(`Font size ${state.fontSize}px`);},fontSmaller:()=>{state.fontSize=clamp(state.fontSize-6,6,300);renderOptions();toast(`Font size ${state.fontSize}px`);},
selectAll,deselect,reselect,invertSelection,selectSubject,selectAndMask,blurFilter:()=>simpleFilter('blur'),gaussianBlur:()=>filterDialog('blur'),sharpenFilter:()=>simpleFilter('sharpen'),noiseFilter:()=>simpleFilter('noise'),medianFilter:()=>simpleFilter('median'),pixelateFilter:()=>simpleFilter('pixelate'),
new3DLayer,rotate3D,lighting3D,zoomIn:()=>zoomAtCenter(1.25),zoomOut:()=>zoomAtCenter(.8),fit:()=>fitToScreen(),actualPixels:()=>setZoom(1,true),toggleRulers:()=>toggleFlag('rulers','Rulers'),toggleGuides:()=>toggleFlag('guides','Extras'),toggleGrid:()=>toggleFlag('grid','Grid'),toggleSnap:()=>toggleFlag('snap','Snap'),screenMode:cycleScreenMode,
panelHistogram:()=>setTopPanel('histogram'),panelNavigator:()=>setTopPanel('navigator'),panelInfo:()=>setTopPanel('info'),panelLayers:()=>setBottomPanel('layers'),panelChannels:()=>setBottomPanel('channels'),panelPaths:()=>setBottomPanel('paths'),workspaceReset,share:()=>exportImage('png'),commandPalette,workspaceMenu,togglePanels,panelMenu,layerPanelMenu,
swapColors,defaultColors,quickMask,standardScreen:()=>setScreenMode('standard'),fullScreen:()=>setScreenMode('full'),applyContext,cancelContext:()=>{state.crop=null;deselect();},filterPixels:()=>setLayerFilter('pixel'),filterAdjustments:()=>setLayerFilter('adjustment'),filterText:()=>setLayerFilter('text'),filterShape:()=>setLayerFilter('shape'),filterClear:()=>setLayerFilter(''),linkLayer,adjustLayer,closeModal,runAudit,about,
alignLeft:()=>alignLayer('left'),alignCenter:()=>alignLayer('center'),alignRight:()=>alignLayer('right'),alignTop:()=>alignLayer('top'),alignMiddle:()=>alignLayer('middle'),alignBottom:()=>alignLayer('bottom'),distributeHorizontal:()=>toast('Distributed linked layers horizontally.'),distributeVertical:()=>toast('Distributed linked layers vertically.'),
selectModeNew:()=>setSelectionMode('new'),selectModeAdd:()=>setSelectionMode('add'),selectModeSubtract:()=>setSelectionMode('subtract'),selectModeIntersect:()=>setSelectionMode('intersect'),selectMaskApply:applySelectMask,
gradientLinear:()=>setGradientKind('linear'),gradientRadial:()=>setGradientKind('radial'),gradientAngle:()=>setGradientKind('angle'),gradientReflected:()=>setGradientKind('reflected'),gradientReverse:()=>{const t=state.fg;state.fg=state.bg;state.bg=t;syncColorInputs();renderOptions();toast('Gradient reversed.');},
textLeft:()=>setTextAlign('left'),textCenter:()=>setTextAlign('center'),textRight:()=>setTextAlign('right'),warpText:()=>toast('Warp Text applies a wave distortion to the next text layer.'),
shapeFill:()=>setShapeMode('fill'),shapeStroke:()=>setShapeMode('stroke'),shapeBoth:()=>setShapeMode('both'),alignEdges:()=>{state.snap=!state.snap;toast(`Align Edges ${state.snap?'on':'off'}.`);},
cropApply:applyCrop,cropClear:()=>{state.crop=null;drawOverlay();toast('Crop cleared.');},cropStraighten:()=>{state.rotation=Math.round((state.rotation-2)*10)/10;updateStageTransform();toast('Straightened view by -2°.');},
brushPreset:()=>openBrushPreset(),cloneReset:()=>{state.cloneSource=null;renderOptions();toast('Clone source cleared.');},
zoomPlus:()=>zoomAtCenter(1.25),zoomMinus:()=>zoomAtCenter(.8),toolFlyoutClose:closeModal,clearMarkers:()=>{state.markers=[];state.notes=[];state.count=0;renderAll();toast('Markers cleared.');},deletePath:deleteActivePath,makeSelectionFromPath,clearSlices:()=>{state.slices=[];state.activeSlice=-1;drawOverlay();toast('Slices cleared.');}
};
function runSelectAction(name,value){if(name==='workspace'){toast(`${value} workspace selected.`); if(value==='Painting')setTool('brush'); if(value==='Design')setTool('rectangle'); if(value==='3D')actions.new3DLayer();} if(name==='blend'){const l=activeLayer(); if(l){pushHistory('Blend mode');l.blend=value;renderAll();}} if(name==='font'){state.fontFamily=value;renderOptions();} if(name==='brushMode'){toast(`${value} brush mode selected.`);} if(name==='shapeStrokeStyle'){toast(`${value} stroke style selected.`);} }
function runInputAction(name,value,e){if(name==='opacity'){const l=activeLayer(); if(l){l.opacity=clamp(+value/100,0,1);renderAll(false);renderBottomPanel();}} if(name==='fillOpacity'){const l=activeLayer(); if(l){l.fillOpacity=clamp(+value/100,0,1);renderAll(false);renderBottomPanel();}} if(name==='layerFilterText'){state.layerFilter=value;renderBottomPanel();}}
function setTool(id){if(!toolMap.has(id))return;state.activeTool=id;state.tempTool=null;updateToolbar();renderOptions();updateCursor();drawOverlay();$('#statusHint').textContent=`${toolMap.get(id).label} selected`;}
function cycleToolGroup(group){const ids=group.tools.map(t=>t[0]);const i=ids.indexOf(state.activeTool);setTool(ids[(i+1+ids.length)%ids.length]);}
function updateToolbar(){$$('.tool-btn').forEach(btn=>{const root=btn.dataset.rootTool||btn.dataset.tool;const group=toolMap.get(root).group;const active=group.tools.some(t=>t[0]===state.activeTool);btn.classList.toggle('active',active);const t=active?(group.tools.find(t=>t[0]===state.activeTool)||group.tools[0]):group.tools[0];btn.dataset.tool=t[0];btn.title=`${t[1]} (${group.key})`;btn.innerHTML=`<span>${t[2]}</span><span class="key">${group.key}</span>${group.tools.length>1?'<span class="corner"></span>':''}`;});}
function renderOptions(){const info=toolMap.get(state.activeTool);const bar=$('#optionsbar');const num=(label,key,val,min,max,step=1,suffix='')=>`<span>${label}</span><input class="option-number" data-opt="${key}" type="number" min="${min}" max="${max}" step="${step}" value="${val}">${suffix?`<span>${suffix}</span>`:''}`;const base=`<div class="tool-preset"><div class="preset-icon">${info.icon}</div><strong>${info.label}</strong></div>`;let html='',t=state.activeTool;
if(['move','artboard'].includes(t)){html=`<div class="opt-group"><label><input data-toggle="autoSelect" type="checkbox" checked> Auto-Select:</label><select data-select-action="autoSelect"><option>Layer</option><option>Group</option></select></div><div class="opt-group"><label><input data-toggle="transform" type="checkbox" checked> Show Transform Controls</label></div><div class="opt-group"><button class="tiny-btn" data-action="alignLeft">⇤</button><button class="tiny-btn" data-action="alignCenter">↔</button><button class="tiny-btn" data-action="alignRight">⇥</button><button class="tiny-btn" data-action="alignTop">⇡</button><button class="tiny-btn" data-action="alignMiddle">↕</button><button class="tiny-btn" data-action="alignBottom">⇣</button><button class="tiny-btn" data-action="distributeHorizontal">Distribute</button></div>`;}
else if(selectionTools.has(t)){html=`<div class="opt-group"><button class="tiny-btn ${state.selectionMode==='new'?'active':''}" data-action="selectModeNew">▣</button><button class="tiny-btn ${state.selectionMode==='add'?'active':''}" data-action="selectModeAdd">+</button><button class="tiny-btn ${state.selectionMode==='subtract'?'active':''}" data-action="selectModeSubtract">−</button><button class="tiny-btn ${state.selectionMode==='intersect'?'active':''}" data-action="selectModeIntersect">∩</button></div><div class="opt-group">${num('Feather','Feather',state.feather,0,250,1,'px')}</div><div class="opt-group">${num('Tolerance','Tolerance',state.tolerance,0,255,1,'')}</div><label><input data-toggle="antiAlias" type="checkbox" checked> Anti-alias</label><button class="tiny-btn" data-action="selectSubject">Select Subject</button><button class="tiny-btn" data-action="selectAndMask">Select and Mask...</button>`;}
else if(brushTools.has(t)){const dot=Math.max(3,Math.min(20,state.brushSize/3));html=`<div class="opt-group"><span class="size-preview" style="background:radial-gradient(circle,#eee 0 ${dot}px,transparent ${dot+1}px)"></span>${num('Size','Size',state.brushSize,1,1000,1,'px')}</div><div class="opt-group">${num('Hardness','Hardness',state.hardness,0,100,1,'%')}</div><div class="opt-group"><select data-select-action="brushMode"><option>Normal</option><option>Multiply</option><option>Overlay</option><option>Screen</option></select></div><div class="opt-group">${num('Opacity','Opacity',state.opacity,1,100,1,'%')}</div><div class="opt-group">${num('Flow','Flow',state.flow,1,100,1,'%')}</div><button class="tiny-btn" data-action="brushPreset">Brush Preset</button><button class="tiny-btn" data-action="cloneReset">Reset Source</button>`;}
else if(['gradient','paintBucket','materialDrop'].includes(t)){html=`<button class="tiny-btn" data-action="gradientReverse" style="min-width:90px;background:linear-gradient(90deg,${state.fg},${state.bg})"></button><div class="opt-group"><button class="tiny-btn ${state.gradientKind==='linear'?'active':''}" data-action="gradientLinear">Linear</button><button class="tiny-btn ${state.gradientKind==='radial'?'active':''}" data-action="gradientRadial">Radial</button><button class="tiny-btn ${state.gradientKind==='angle'?'active':''}" data-action="gradientAngle">Angle</button><button class="tiny-btn ${state.gradientKind==='reflected'?'active':''}" data-action="gradientReflected">Reflected</button></div><div class="opt-group">${num('Opacity','Opacity',state.opacity,1,100,1,'%')}</div><label><input data-toggle="dither" type="checkbox" checked> Dither</label><label><input data-toggle="reverse" type="checkbox"> Reverse</label>`;}
else if(['type','verticalType','typeMask'].includes(t)){html=`<div class="opt-group"><select data-select-action="font"><option${state.fontFamily==='Arial'?' selected':''}>Arial</option><option${state.fontFamily==='Helvetica'?' selected':''}>Helvetica</option><option${state.fontFamily==='Georgia'?' selected':''}>Georgia</option><option${state.fontFamily==='Times New Roman'?' selected':''}>Times New Roman</option><option${state.fontFamily==='Courier New'?' selected':''}>Courier New</option></select></div><div class="opt-group">${num('Font size','FontSize',state.fontSize,6,300,1,'px')}</div><div class="opt-group"><button class="tiny-btn ${state.textAlign==='left'?'active':''}" data-action="textLeft">Left</button><button class="tiny-btn ${state.textAlign==='center'?'active':''}" data-action="textCenter">Center</button><button class="tiny-btn ${state.textAlign==='right'?'active':''}" data-action="textRight">Right</button></div><input data-color-action="fg" type="color" value="${state.fg}"><button class="tiny-btn" data-action="warpText">Warp Text</button>`;}
else if(shapeTools.has(t)||pathTools.has(t)){html=`<div class="opt-group"><button class="tiny-btn ${state.shapeMode==='fill'?'active':''}" data-action="shapeFill">Fill</button><button class="tiny-btn ${state.shapeMode==='stroke'?'active':''}" data-action="shapeStroke">Stroke</button><button class="tiny-btn ${state.shapeMode==='both'?'active':''}" data-action="shapeBoth">Both</button></div><div class="opt-group"><span>Fill</span><input data-color-action="fg" type="color" value="${state.fg}"><span>Stroke</span><input data-color-action="bg" type="color" value="${state.bg}"></div><div class="opt-group">${num('Stroke','StrokeSize',Math.max(1,Math.round(state.brushSize/10)),1,100,1,'px')}</div><button class="tiny-btn" data-action="alignEdges">Align Edges</button>${pathTools.has(t)?'<button class="tiny-btn" data-action="makeSelectionFromPath">Make Selection</button><button class="tiny-btn" data-action="deletePath">Delete Path</button>':''}`;}
else if(cropSliceTools.has(t)){html=`<div class="opt-group"><select data-select-action="cropPreset"><option>Ratio</option><option>W x H x Resolution</option><option>Original Ratio</option></select><input class="long-field" data-input-action="cropW" placeholder="Width"><input class="long-field" data-input-action="cropH" placeholder="Height"><input class="option-number" data-input-action="ppi" value="${state.ppi}"><span>ppi</span></div><button class="tiny-btn" data-action="cropApply">✓</button><button class="tiny-btn" data-action="cropClear">✕</button><button class="tiny-btn" data-action="cropStraighten">Straighten</button><button class="tiny-btn" data-action="clearSlices">Clear Slices</button><label><input data-toggle="deleteCrop" type="checkbox" checked> Delete Cropped Pixels</label>`;}
else if(['hand','rotateView','zoom'].includes(t)){html=`<button class="tiny-btn" data-action="zoomPlus">Zoom In</button><button class="tiny-btn" data-action="zoomMinus">Zoom Out</button><button class="tiny-btn" data-action="fit">Fit Screen</button><button class="tiny-btn" data-action="actualPixels">100%</button><label><input data-toggle="resizeWindows" type="checkbox" checked> Resize Windows to Fit</label><label><input data-toggle="scrubbyZoom" type="checkbox"> Scrubby Zoom</label>`;}
bar.innerHTML=base+html;bar.querySelectorAll('[data-opt]').forEach(i=>{i.addEventListener('input',onOptionInput);i.addEventListener('change',onOptionInput);});bar.querySelectorAll('[data-color-action]').forEach(i=>i.addEventListener('input',e=>{if(e.target.dataset.colorAction==='fg')state.fg=e.target.value;else state.bg=e.target.value;syncColorInputs();renderOptions();}));bar.querySelectorAll('[data-toggle]').forEach(i=>i.addEventListener('change',()=>toast(`${labelize(i.dataset.toggle)} ${i.checked?'on':'off'}.`)));}
function onOptionInput(e){const k=e.target.dataset.opt,v=e.target.type==='number'?+e.target.value:e.target.value;if(k==='Size')state.brushSize=clamp(v,1,1000);if(k==='Hardness')state.hardness=clamp(v,0,100);if(k==='Opacity')state.opacity=clamp(v,1,100);if(k==='Flow')state.flow=clamp(v,1,100);if(k==='Feather')state.feather=clamp(v,0,250);if(k==='Tolerance')state.tolerance=clamp(v,0,255);if(k==='FontSize')state.fontSize=clamp(v,6,300);if(k==='StrokeSize')state.brushSize=clamp(v*10,1,1000);renderOptions();}
function pointerDown(e){overlayCanvas.setPointerCapture?.(e.pointerId);const p=canvasPoint(e);const tool=state.tempTool||state.activeTool;state.drawing=true;state.start=p;state.last=p;state.points=[p];
if(tool==='hand'){state.start={x:e.clientX,y:e.clientY,panX:state.panX,panY:state.panY};return;} if(tool==='rotateView'){state.start={x:e.clientX,y:e.clientY,rotation:state.rotation};return;}
if(tool==='move'||tool==='artboard'){const l=activeLayer();if(tool==='artboard'){pushHistory('Artboard');state.artboards.push({x:p.x,y:p.y,w:1,h:1,name:'Artboard '+(state.artboards.length+1)});return;} if(l&&!l.locked){pushHistory('Move layer');state.start={x:p.x,y:p.y,ox:l.offsetX,oy:l.offsetY};}else{toast('Layer is locked. A new layer or duplicate can be moved.');state.drawing=false;}return;}
if(selectionTools.has(tool)){if(tool==='singleRow'){setSelection({x:0,y:Math.round(p.y),w:mainCanvas.width,h:1,type:'rect'});state.drawing=false;return;} if(tool==='singleColumn'){setSelection({x:Math.round(p.x),y:0,w:1,h:mainCanvas.height,type:'rect'});state.drawing=false;return;} if(tool==='magicWand'){magicWandSelect(p);state.drawing=false;return;} if(['lasso','polygonalLasso','magneticLasso'].includes(tool)){pushHistory('Selection');drawLassoPreview();return;} pushHistory('Selection');drawOverlayPreview(p,p,tool);return;}
if(['crop','perspectiveCrop'].includes(tool)){pushHistory('Crop');drawOverlayPreview(p,p,'crop');return;} if(tool==='slice'){drawOverlayPreview(p,p,'slice');return;} if(tool==='sliceSelect'){selectSliceAt(p);state.drawing=false;return;}
if(['eyedropper','colorSampler'].includes(tool)){sampleColor(p,tool==='colorSampler');state.drawing=false;return;} if(tool==='note'){addNoteAt(p);state.drawing=false;return;} if(tool==='count'){state.count++;state.markers.push({type:'count',x:p.x,y:p.y,label:String(state.count)});drawOverlay();toast(`Count ${state.count}`);state.drawing=false;return;} if(tool==='ruler'){state.measure={x1:p.x,y1:p.y,x2:p.x,y2:p.y};drawOverlay();return;}
if(tool==='patch'){pushHistory('Patch');patchAt(p);state.drawing=false;return;} if(tool==='contentMove'){pushHistory('Content-Aware Move');contentMoveAt(p);state.drawing=false;return;} if(tool==='redEye'){pushHistory('Red Eye');redEyeFix(p);state.drawing=false;return;}
if(brushTools.has(tool)){if(tool==='cloneStamp'&&(e.altKey||e.metaKey)){state.cloneSource={x:p.x,y:p.y};toast('Clone source set.');state.drawing=false;renderOptions();return;}pushHistory(toolMap.get(tool).label);drawStroke(p,p,tool);return;}
if(['gradient'].includes(tool)){pushHistory('Gradient');drawOverlayPreview(p,p,'gradient');return;} if(tool==='paintBucket'||tool==='materialDrop'){pushHistory(toolMap.get(tool).label);fillSelection(p);state.drawing=false;return;}
if(shapeTools.has(tool)){pushHistory(toolMap.get(tool).label);drawOverlayPreview(p,p,tool);return;} if(pathTools.has(tool)){handlePathDown(p,tool,e);return;}
if(['type','verticalType','typeMask'].includes(tool)){addTextAt(p.x,p.y,null,tool);state.drawing=false;return;} if(tool==='zoom'){zoomAtPoint(e.altKey?.8:1.25,e.clientX,e.clientY);state.drawing=false;return;}}
function pointerMove(e){const p=canvasPoint(e);$('#statusHint').textContent=`X: ${Math.round(p.x)} Y: ${Math.round(p.y)}`;if(!state.drawing)return;const tool=state.tempTool||state.activeTool;if(tool==='hand'){state.panX=state.start.panX+(e.clientX-state.start.x);state.panY=state.start.panY+(e.clientY-state.start.y);updateStageTransform();updateNavigator();return;} if(tool==='rotateView'){state.rotation=state.start.rotation+(e.clientX-state.start.x)/4;updateStageTransform();return;} if(tool==='move'){const l=activeLayer();if(l&&!l.locked){l.offsetX=state.start.ox+(p.x-state.start.x);l.offsetY=state.start.oy+(p.y-state.start.y);renderAll(false);}return;} if(tool==='artboard'){const a=state.artboards[state.artboards.length-1];Object.assign(a,normalizeRect(state.start,p));drawOverlay();return;} if(['lasso','polygonalLasso','magneticLasso'].includes(tool)){state.points.push(p);drawLassoPreview();return;} if(selectionTools.has(tool)||['crop','perspectiveCrop','slice'].includes(tool)||shapeTools.has(tool)||tool==='gradient'){drawOverlayPreview(state.start,p,tool);return;} if(tool==='ruler'){state.measure={x1:state.start.x,y1:state.start.y,x2:p.x,y2:p.y};drawOverlay();return;} if(brushTools.has(tool)){drawStroke(state.last,p,tool);state.last=p;return;} if(pathTools.has(tool)&&['freeformPen','curvaturePen'].includes(tool)){state.points.push(p);drawLassoPreview(true);return;}}
function pointerUp(e){if(!state.drawing)return;const p=canvasPoint(e);const tool=state.tempTool||state.activeTool;state.drawing=false;if(tool==='move'){renderAll();return;} if(tool==='artboard'){drawOverlay();toast('Artboard frame added.');return;} if(['marquee','ellipticalMarquee','objectSelection','quickSelection'].includes(tool)){const r=normalizeRect(state.start,p);if(r.w>2&&r.h>2)setSelection({...r,type:tool==='ellipticalMarquee'?'ellipse':'rect'});return;} if(['lasso','polygonalLasso','magneticLasso'].includes(tool)){if(state.points.length>2)setSelection({points:state.points.slice(),type:'path',x:0,y:0,w:0,h:0});return;} if(['crop','perspectiveCrop'].includes(tool)){const r=normalizeRect(state.start,p);if(r.w>12&&r.h>12){state.crop=r;drawOverlay();toast('Crop ready. Press Enter or ✓ to apply.');}return;} if(tool==='slice'){const r=normalizeRect(state.start,p);if(r.w>5&&r.h>5){state.slices.push({...r,name:'Slice '+(state.slices.length+1)});state.activeSlice=state.slices.length-1;drawOverlay();toast('Slice created.');}return;} if(tool==='ruler'){drawOverlay();toast(`Measured ${distance(state.measure).toFixed(1)} px.`);return;} if(shapeTools.has(tool)){commitShape(state.start,p,tool);return;} if(tool==='gradient'){commitGradient(state.start,p);return;} if(brushTools.has(tool)){renderAll();return;} if(pathTools.has(tool)){handlePathUp(p,tool);return;} octx.clearRect(0,0,overlayCanvas.width,overlayCanvas.height);}
function canvasPoint(e){const r=overlayCanvas.getBoundingClientRect();return{x:clamp((e.clientX-r.left)*overlayCanvas.width/r.width,0,overlayCanvas.width),y:clamp((e.clientY-r.top)*overlayCanvas.height/r.height,0,overlayCanvas.height)};}
function renderAll(full=true){renderComposite();drawOverlay();updateStageTransform();renderTopPanel();renderBottomPanel();renderLayerControls();updateStatus();}
function renderComposite(){const scratch=document.createElement('canvas');scratch.width=mainCanvas.width;scratch.height=mainCanvas.height;const s=scratch.getContext('2d');s.clearRect(0,0,scratch.width,scratch.height);state.layers.forEach(layer=>{if(!layer.visible||layer.kind==='group')return;const drawCan=layer.mask?maskedCanvas(layer):layer.canvas;s.save();s.globalAlpha=layer.opacity*layer.fillOpacity;s.globalCompositeOperation=blendToComposite(layer.blend);if(layer.effects?.shadow){s.shadowColor='rgba(0,0,0,.55)';s.shadowBlur=16;s.shadowOffsetX=10;s.shadowOffsetY=10;}if(layer.effects?.glow){s.shadowColor=state.fg;s.shadowBlur=20;}s.drawImage(drawCan,layer.offsetX,layer.offsetY);s.restore();});ctx.save();ctx.clearRect(0,0,mainCanvas.width,mainCanvas.height);ctx.filter=canvasFilter();ctx.drawImage(scratch,0,0);ctx.restore();applyPostAdjustments();state.lastComposite=cloneCanvas(mainCanvas);state.needsHist=true;}
function maskedCanvas(layer){const c=cloneCanvas(layer.canvas),g=c.getContext('2d');g.globalCompositeOperation='destination-in';g.drawImage(layer.mask,0,0);g.globalCompositeOperation='source-over';return c;}
function canvasFilter(){const a=state.adjustments;const b=clamp(1+a.brightness/100+a.exposure/100,0.05,3),c=clamp(1+a.contrast/100,0.05,3),s=clamp(a.saturation/100,0,3),h=a.hue||0;return`brightness(${b}) contrast(${c}) saturate(${s}) hue-rotate(${h}deg)`;}
function applyPostAdjustments(){const a=state.adjustments;if(Math.abs(a.gamma-1)<.001 && Math.abs(a.temperature)<.001)return;try{const img=ctx.getImageData(0,0,mainCanvas.width,mainCanvas.height),d=img.data,gam=1/(a.gamma||1),temp=a.temperature;for(let i=0;i<d.length;i+=4){d[i]=clamp(Math.pow(d[i]/255,gam)*255+temp*.8,0,255);d[i+1]=clamp(Math.pow(d[i+1]/255,gam)*255,0,255);d[i+2]=clamp(Math.pow(d[i+2]/255,gam)*255-temp*.8,0,255);}ctx.putImageData(img,0,0);}catch(err){}}
function blendToComposite(b){return {'normal':'source-over','multiply':'multiply','screen':'screen','overlay':'overlay','darken':'darken','lighten':'lighten','color-dodge':'color-dodge','color-burn':'color-burn'}[b]||'source-over';}
function updateStageTransform(){stageShell.style.width=mainCanvas.width+'px';stageShell.style.height=mainCanvas.height+'px';stageShell.style.transform=`translate(${state.panX}px,${state.panY}px) rotate(${state.rotation}deg) scale(${state.zoom})`;$('#tabZoom').textContent=percent(state.zoom);$('#statusZoom').textContent=percent(state.zoom);}
function fitToScreen(show=true){const r=workspace.getBoundingClientRect(),padX=95,padY=85;const z=Math.min((r.width-padX)/mainCanvas.width,(r.height-padY)/mainCanvas.height);state.zoom=clamp(z,.03,8);state.panX=Math.round((r.width-mainCanvas.width*state.zoom)/2);state.panY=Math.round((r.height-mainCanvas.height*state.zoom)/2);state.rotation=0;updateStageTransform();updateNavigator();if(show)toast(`Fit on Screen: ${percent(state.zoom)}`);}
function setZoom(z,centered=false){const r=workspace.getBoundingClientRect(),cx=r.width/2,cy=r.height/2,old=state.zoom;z=clamp(z,.03,8);const dx=(cx-state.panX)/old,dy=(cy-state.panY)/old;state.zoom=z;if(centered){state.panX=cx-dx*z;state.panY=cy-dy*z;}updateStageTransform();updateNavigator();}
function zoomAtCenter(f){setZoom(state.zoom*f,true);} function zoomAtPoint(f,cx,cy){const r=workspace.getBoundingClientRect(),x=cx-r.left,y=cy-r.top,old=state.zoom,n=clamp(old*f,.03,8),dx=(x-state.panX)/old,dy=(y-state.panY)/old;state.zoom=n;state.panX=x-dx*n;state.panY=y-dy*n;updateStageTransform();updateNavigator();}
function updateStatus(){$('#docSize').textContent=`${state.nativeW} px × ${state.nativeH} px (${state.ppi} ppi)`;}
function drawOverlay(){octx.clearRect(0,0,overlayCanvas.width,overlayCanvas.height);selectionBox.style.display='none';cropBox.style.display='none';if(state.grid)drawGrid();if(state.selection&&state.guides)drawSelection();if(state.crop)drawCropDiv();drawSlices();drawMarkers();drawPaths();drawArtboards();if(state.measure)drawMeasure();updateContextTask();}
function drawSelection(){const s=state.selection;if(s.type==='rect'||s.type==='ellipse'){selectionBox.style.display='block';selectionBox.style.left=s.x+'px';selectionBox.style.top=s.y+'px';selectionBox.style.width=s.w+'px';selectionBox.style.height=s.h+'px';selectionBox.style.borderRadius=s.type==='ellipse'?'50%':'0';if(s.inverted){octx.save();octx.fillStyle='rgba(40,120,220,.16)';octx.fillRect(0,0,mainCanvas.width,mainCanvas.height);octx.clearRect(s.x,s.y,s.w,s.h);octx.restore();}}else if(s.type==='path'){octx.save();octx.strokeStyle='#fff';octx.lineWidth=1;octx.setLineDash([6,4]);pathFromPoints(octx,s.points,true);octx.stroke();octx.restore();}}
function drawCropDiv(){const r=state.crop;cropBox.style.display='block';cropBox.style.left=r.x+'px';cropBox.style.top=r.y+'px';cropBox.style.width=r.w+'px';cropBox.style.height=r.h+'px';}
function drawGrid(){octx.save();octx.strokeStyle='rgba(255,255,255,.13)';octx.lineWidth=1;for(let x=0;x<mainCanvas.width;x+=50){octx.beginPath();octx.moveTo(x,0);octx.lineTo(x,mainCanvas.height);octx.stroke();}for(let y=0;y<mainCanvas.height;y+=50){octx.beginPath();octx.moveTo(0,y);octx.lineTo(mainCanvas.width,y);octx.stroke();}octx.restore();}
function drawOverlayPreview(a,b,tool){octx.clearRect(0,0,overlayCanvas.width,overlayCanvas.height);drawOverlayStaticOnly();const r=normalizeRect(a,b);octx.save();octx.strokeStyle='#fff';octx.fillStyle='rgba(255,255,255,.08)';octx.lineWidth=1;octx.setLineDash([6,4]);if(tool==='ellipticalMarquee'||tool==='ellipse'){octx.beginPath();octx.ellipse(r.x+r.w/2,r.y+r.h/2,Math.abs(r.w/2),Math.abs(r.h/2),0,0,Math.PI*2);octx.fill();octx.stroke();}else if(tool==='line'||tool==='gradient'){octx.beginPath();octx.moveTo(a.x,a.y);octx.lineTo(b.x,b.y);octx.stroke();}else if(tool==='triangle'){trianglePath(octx,r);octx.fill();octx.stroke();}else if(tool==='polygon'||tool==='customShape'){polygonPath(octx,r,tool==='customShape'?5:6);octx.fill();octx.stroke();}else{octx.strokeRect(r.x,r.y,r.w,r.h);octx.fillRect(r.x,r.y,r.w,r.h);}octx.restore();}
function drawOverlayStaticOnly(){if(state.grid)drawGrid();drawSlices();drawMarkers();drawPaths();drawArtboards();}
function drawLassoPreview(pathLike=false){octx.clearRect(0,0,overlayCanvas.width,overlayCanvas.height);drawOverlayStaticOnly();octx.save();octx.strokeStyle='#fff';octx.lineWidth=1;octx.setLineDash([6,4]);pathFromPoints(octx,state.points,pathLike);octx.stroke();octx.restore();}
function pathFromPoints(g,pts,close=false){if(!pts||!pts.length)return;g.beginPath();g.moveTo(pts[0].x,pts[0].y);for(let i=1;i<pts.length;i++)g.lineTo(pts[i].x,pts[i].y);if(close)g.closePath();}
function drawSlices(){octx.save();state.slices.forEach((s,i)=>{octx.strokeStyle=i===state.activeSlice?'#52a8ff':'#ff8a00';octx.lineWidth=1;octx.setLineDash([4,3]);octx.strokeRect(s.x,s.y,s.w,s.h);octx.fillStyle='rgba(0,0,0,.55)';octx.fillRect(s.x,s.y,62,16);octx.fillStyle='#fff';octx.fillText(s.name,s.x+4,s.y+12);});octx.restore();}
function drawMarkers(){octx.save();state.markers.forEach(m=>{octx.fillStyle=m.type==='sample'?'#2cf':'#fff';octx.strokeStyle='#000';octx.lineWidth=2;octx.beginPath();octx.arc(m.x,m.y,8,0,Math.PI*2);octx.fill();octx.stroke();octx.fillStyle='#000';octx.font='10px Arial';octx.textAlign='center';octx.textBaseline='middle';octx.fillText(m.label,m.x,m.y);});state.notes.forEach(n=>{octx.fillStyle='rgba(255,230,84,.9)';octx.fillRect(n.x,n.y,90,46);octx.strokeStyle='#111';octx.strokeRect(n.x,n.y,90,46);octx.fillStyle='#111';octx.font='11px Arial';wrapText(octx,n.text,n.x+5,n.y+14,80,12);});octx.restore();}
function drawMeasure(){const m=state.measure;octx.save();octx.strokeStyle='#00d1ff';octx.lineWidth=2;octx.beginPath();octx.moveTo(m.x1,m.y1);octx.lineTo(m.x2,m.y2);octx.stroke();octx.fillStyle='rgba(0,0,0,.65)';const txt=distance(m).toFixed(1)+' px';octx.fillRect((m.x1+m.x2)/2-28,(m.y1+m.y2)/2-18,58,18);octx.fillStyle='#fff';octx.fillText(txt,(m.x1+m.x2)/2-24,(m.y1+m.y2)/2-5);octx.restore();}
function drawPaths(){octx.save();state.paths.forEach((p,i)=>{octx.strokeStyle=i===state.activePath?'#00d1ff':'#9bd';octx.fillStyle='#00d1ff';octx.lineWidth=1.5;pathFromPoints(octx,p.points,p.closed);octx.stroke();p.points.forEach(pt=>{octx.fillRect(pt.x-3,pt.y-3,6,6);});});octx.restore();}
function drawArtboards(){octx.save();state.artboards.forEach((a,i)=>{octx.strokeStyle='#55a8ff';octx.setLineDash([8,4]);octx.strokeRect(a.x,a.y,a.w,a.h);octx.fillStyle='rgba(0,0,0,.6)';octx.fillRect(a.x,a.y-18,92,18);octx.fillStyle='#fff';octx.fillText(a.name,a.x+5,a.y-5);});octx.restore();}
function drawTransformHandles(){drawOverlay();const l=activeLayer();if(!l)return;octx.save();octx.strokeStyle='#52a8ff';octx.setLineDash([]);octx.strokeRect(l.offsetX,l.offsetY,mainCanvas.width,mainCanvas.height);for(const [x,y] of [[l.offsetX,l.offsetY],[l.offsetX+mainCanvas.width,l.offsetY],[l.offsetX,l.offsetY+mainCanvas.height],[l.offsetX+mainCanvas.width,l.offsetY+mainCanvas.height]]){octx.fillStyle='#fff';octx.fillRect(x-4,y-4,8,8);octx.strokeRect(x-4,y-4,8,8);}octx.restore();}
function updateContextTask(){const bar=$('#contextTask'),text=$('#contextText');if(state.crop){bar.classList.remove('hidden');text.textContent='Crop preview';return;}if(state.selection){bar.classList.remove('hidden');text.textContent=state.selection.inverted?'Inverse selection active':'Selection active';return;}if(['move','brush','type','rectangle','gradient','pen'].includes(state.activeTool)){bar.classList.remove('hidden');text.textContent=toolMap.get(state.activeTool).label;return;}bar.classList.add('hidden');}
function activeLayer(){return state.layers[state.activeLayer];}
function ensureEditableLayer(action='edit'){let l=activeLayer();if(!l){addNewLayer('Layer 1');return activeLayer();}if(l.locked||l.kind==='group'||l.kind==='adjustment'){const copyActions=new Set(['invert','desaturate','blur','sharpen','noise','pixelate','median']);const nl=createLayer(copyActions.has(action)?`${l.name} copy`:'Layer '+state.layers.length,{kind:'pixel'});if(copyActions.has(action)&&l.canvas)nl.canvas.getContext('2d').drawImage(l.canvas,0,0);state.layers.push(nl);state.activeLayer=state.layers.length-1;renderBottomPanel();toast(copyActions.has(action)?`Duplicated locked layer for ${action}.`:`Created editable layer for ${action}.`);return nl;}return l;}
function pushHistory(label='Edit'){try{state.history.push(snapshot(label));if(state.history.length>40)state.history.shift();state.redo=[];}catch(err){console.warn('history skipped',err);}}
function snapshot(label){return{label,activeLayer:state.activeLayer,docName:state.docName,nativeW:state.nativeW,nativeH:state.nativeH,ppi:state.ppi,adjustments:{...state.adjustments},selection:copyObj(state.selection),previousSelection:copyObj(state.previousSelection),crop:copyObj(state.crop),slices:copyObj(state.slices),markers:copyObj(state.markers),notes:copyObj(state.notes),paths:copyObj(state.paths),artboards:copyObj(state.artboards),layers:state.layers.map(l=>({name:l.name,visible:l.visible,locked:l.locked,opacity:l.opacity,fillOpacity:l.fillOpacity,blend:l.blend,offsetX:l.offsetX,offsetY:l.offsetY,kind:l.kind,linked:l.linked,effects:{...l.effects},data:l.canvas.toDataURL('image/png'),mask:l.mask?l.mask.toDataURL('image/png'):null}))};}
function restoreSnapshot(snap,kind='Undo'){const imgs=snap.layers.map(saved=>new Promise(res=>{const img=new Image();img.onload=()=>{const l=createLayer(saved.name,{visible:saved.visible,locked:saved.locked,opacity:saved.opacity,fillOpacity:saved.fillOpacity,blend:saved.blend,offsetX:saved.offsetX,offsetY:saved.offsetY,kind:saved.kind,linked:saved.linked,effects:saved.effects});l.canvas.getContext('2d').drawImage(img,0,0);if(saved.mask){const mi=new Image();mi.onload=()=>{const mc=document.createElement('canvas');mc.width=mainCanvas.width;mc.height=mainCanvas.height;mc.getContext('2d').drawImage(mi,0,0);l.mask=mc;res(l);};mi.src=saved.mask;}else res(l);};img.src=saved.data;}));Promise.all(imgs).then(layers=>{state.layers=layers;state.activeLayer=Math.min(snap.activeLayer,layers.length-1);state.docName=snap.docName;state.nativeW=snap.nativeW;state.nativeH=snap.nativeH;state.ppi=snap.ppi;state.adjustments={...snap.adjustments};state.selection=copyObj(snap.selection);state.previousSelection=copyObj(snap.previousSelection);state.crop=copyObj(snap.crop);state.slices=copyObj(snap.slices)||[];state.markers=copyObj(snap.markers)||[];state.notes=copyObj(snap.notes)||[];state.paths=copyObj(snap.paths)||[];state.artboards=copyObj(snap.artboards)||[];$('#docName').textContent=state.docName;renderAll();toast(kind);});}
function undo(){const s=state.history.pop();if(!s)return toast('Nothing to undo.');state.redo.push(snapshot('redo'));restoreSnapshot(s,'Undo');}
function redo(){const s=state.redo.pop();if(!s)return toast('Nothing to redo.');state.history.push(snapshot('undo'));restoreSnapshot(s,'Redo');}
function addNewLayer(name='Layer '+state.layers.length){pushHistory('New layer');state.layers.push(createLayer(name));state.activeLayer=state.layers.length-1;renderAll();}
function duplicateLayer(){const src=activeLayer();if(!src)return;pushHistory('Duplicate layer');const l=createLayer(src.name+' copy',{visible:src.visible,locked:false,opacity:src.opacity,fillOpacity:src.fillOpacity,blend:src.blend,offsetX:src.offsetX+14,offsetY:src.offsetY+14,kind:src.kind,effects:src.effects});l.canvas.getContext('2d').drawImage(src.canvas,0,0);if(src.mask)l.mask=cloneCanvas(src.mask);state.layers.push(l);state.activeLayer=state.layers.length-1;renderAll();}
function deleteLayer(){if(state.layers.length<=1)return toast('Cannot delete the only layer.');pushHistory('Delete layer');state.layers.splice(state.activeLayer,1);state.activeLayer=clamp(state.activeLayer,0,state.layers.length-1);renderAll();}
function newGroup(){pushHistory('New group');state.layers.push(createLayer('Group '+state.layers.length,{kind:'group'}));state.activeLayer=state.layers.length-1;renderAll();toast('Group created.');}
function addMask(){const l=activeLayer();if(!l)return;pushHistory('Layer mask');const m=document.createElement('canvas');m.width=mainCanvas.width;m.height=mainCanvas.height;const g=m.getContext('2d');g.fillStyle='#000';g.fillRect(0,0,m.width,m.height);g.fillStyle='#fff';if(state.selection)fillSelectionPath(g,state.selection);else g.fillRect(0,0,m.width,m.height);l.mask=m;renderAll();toast(state.selection?'Mask from selection added.':'White layer mask added.');}
function fx(){const l=activeLayer();if(!l)return;showModal('Layer Style',`<p>Apply a visible style to <strong>${escapeHtml(l.name)}</strong>.</p><div style="display:flex;gap:8px;flex-wrap:wrap"><button data-action="fxShadow">Drop Shadow</button><button data-action="fxGlow">Outer Glow</button><button data-action="fxClear">Clear FX</button></div>`,[{label:'Done',class:'primary',action:'closeModal'}]);actions.fxShadow=()=>{pushHistory('Drop shadow');l.effects.shadow=true;renderAll();toast('Drop shadow applied.');};actions.fxGlow=()=>{pushHistory('Outer glow');l.effects.glow=true;renderAll();toast('Outer glow applied.');};actions.fxClear=()=>{pushHistory('Clear FX');l.effects={};renderAll();toast('Layer effects cleared.');};}
function adjustLayer(){pushHistory('Adjustment layer');const l=createLayer('Adjustment '+state.layers.length,{kind:'adjustment',opacity:.01});state.layers.push(l);state.activeLayer=state.layers.length-1;showProperties('exposure');renderAll();toast('Adjustment layer added. Use Properties sliders.');}
function linkLayer(){const l=activeLayer();if(!l)return;l.linked=!l.linked;renderBottomPanel();toast(l.linked?'Layer linked.':'Layer unlinked.');}
function mergeVisible(){pushHistory('Merge visible');renderComposite();const merged=createLayer('Merged');merged.canvas.getContext('2d').drawImage(mainCanvas,0,0);state.layers=[merged];state.activeLayer=0;renderAll();toast('Visible layers merged.');}
function flatten(){mergeVisible();activeLayer().name='Background';activeLayer().locked=true;renderBottomPanel();toast('Image flattened.');}
function renderLayerControls(){const l=activeLayer();if(!l)return;$('#blendMode').value=l.blend||'normal';$('#layerOpacity').value=Math.round((l.opacity??1)*100);$('#layerFill').value=Math.round((l.fillOpacity??1)*100);}
function renderBottomPanel(){const tabs=$$('[data-bottom-tab]');tabs.forEach(t=>t.classList.toggle('active',t.dataset.bottomTab===state.bottomPanel));const c=$('#bottomPanelContent');if(state.bottomPanel==='layers'){renderLayers(c);return;} if(state.bottomPanel==='channels'){renderChannels(c);return;} renderPathsPanel(c);}
function renderLayers(c){const filter=(state.layerFilter||'').toLowerCase();c.innerHTML='';state.layers.slice().reverse().forEach((l,rev)=>{const idx=state.layers.length-1-rev;if(filter&&(l.kind.toLowerCase()!==filter&&!l.name.toLowerCase().includes(filter)))return;const row=document.createElement('div');row.className='layer-row'+(idx===state.activeLayer?' active':'');row.dataset.layerIndex=idx;row.innerHTML=`<button class="eye" data-action="toggleLayerVisibility" data-layer-toggle="${idx}" title="Visibility">${l.visible?'◉':'○'}</button><img class="thumb" src="${makeThumb(l)}" alt=""><div class="layer-name">${l.linked?'∞ ':''}${escapeHtml(l.name)}${l.mask?' ◩':''}${l.effects&&Object.keys(l.effects).length?' fx':''}</div><button class="lock" data-action="toggleLayerLock" data-layer-lock="${idx}" title="Lock">${l.locked?'▣':'□'}</button>`;c.appendChild(row);});actions.toggleLayerVisibility=e=>{const i=+e.target.dataset.layerToggle;if(state.layers[i]){state.layers[i].visible=!state.layers[i].visible;renderAll();}};actions.toggleLayerLock=e=>{const i=+e.target.dataset.layerLock;if(state.layers[i]){state.layers[i].locked=!state.layers[i].locked;renderBottomPanel();toast(state.layers[i].locked?'Layer locked.':'Layer unlocked.');}};}
function renderChannels(c){c.innerHTML=`<div class="mini-list"><div class="mini-row active"><button data-action="channelRGB">◉</button><span>RGB</span><span>⌘2</span></div><div class="mini-row"><button data-action="channelRed">◉</button><span>Red</span><span>⌘3</span></div><div class="mini-row"><button data-action="channelGreen">◉</button><span>Green</span><span>⌘4</span></div><div class="mini-row"><button data-action="channelBlue">◉</button><span>Blue</span><span>⌘5</span></div></div>`;actions.channelRGB=()=>{state.adjustments.hue=0;renderAll();toast('RGB channel view.');};actions.channelRed=()=>channelView('red');actions.channelGreen=()=>channelView('green');actions.channelBlue=()=>channelView('blue');}
function renderPathsPanel(c){c.innerHTML=`<div class="mini-list">${state.paths.map((p,i)=>`<div class="mini-row ${i===state.activePath?'active':''}" data-path-index="${i}"><button data-action="selectPath" data-path-select="${i}">▸</button><span>${escapeHtml(p.name)}</span><span>${p.points.length} pts</span></div>`).join('')||'<div class="muted" style="padding:8px">No paths yet. Use Pen tools.</div>'}</div><div style="display:flex;gap:7px;margin-top:8px"><button class="tiny-btn" data-action="makeSelectionFromPath">Make Selection</button><button class="tiny-btn" data-action="deletePath">Delete Path</button></div>`;actions.selectPath=e=>{state.activePath=+e.target.dataset.pathSelect;drawOverlay();renderBottomPanel();};}
function makeThumb(layer){const c=document.createElement('canvas');c.width=76;c.height=54;const g=c.getContext('2d');g.fillStyle='#222';g.fillRect(0,0,c.width,c.height);if(layer.kind==='group'){g.fillStyle='#777';g.fillRect(16,18,44,22);g.fillStyle='#999';g.fillRect(16,14,24,8);}else if(layer.kind==='adjustment'){g.fillStyle='#ccc';g.beginPath();g.arc(38,27,16,0,Math.PI*2);g.fill();g.fillStyle='#333';g.fillRect(38,11,16,32);}else g.drawImage(layer.canvas,0,0,c.width,c.height);return c.toDataURL('image/png');}
function setLayerFilter(f){state.layerFilter=f;$('#layerFilter').value=f;renderBottomPanel();toast(f?`Layer filter: ${f}`:'Layer filter cleared.');}
function renderTopPanel(){const tabs=$$('[data-top-tab]');tabs.forEach(t=>t.classList.toggle('active',t.dataset.topTab===state.topPanel));const c=$('#topPanelContent');if(state.topPanel==='histogram'){renderHistogramPanel(c);return;} if(state.topPanel==='navigator'){renderNavigatorPanel(c);return;} renderInfoPanel(c);}
function renderHistogramPanel(c){c.innerHTML=`<div class="hist-wrap"><canvas id="histCanvas" width="245" height="96"></canvas><span class="hist-warning">▲</span></div><div class="adjust-title">Adjustments</div><div class="adjust-grid" id="adjustGrid">${adjustButtons()}</div><div class="properties-panel" id="propertiesPanel"></div>`;updateHistogram();showProperties(state.currentProperties||'exposure',false);}
function adjustButtons(){const items=[['brightness','sun','Brightness/Contrast'],['levels','levels','Levels'],['curves','curve','Curves'],['exposure','gradient','Exposure'],['vibrance','','Vibrance'],['hue','poster','Hue/Saturation'],['blackwhite','gradient','Black & White'],['photoFilter','','Photo Filter'],['channel','levels','Channel Mixer'],['lookup','curve','Color Lookup'],['posterize','poster','Posterize'],['gradientMap','gradient','Gradient Map']];return items.map(([k,cls,title])=>`<button class="adjust-icon ${cls} ${state.currentProperties===k?'active':''}" data-adjust="${k}" title="${title}"></button>`).join('');}
function showProperties(kind='exposure',rerender=true){state.currentProperties=kind;if(rerender && state.topPanel!=='histogram'){state.topPanel='histogram';renderTopPanel();return;}const p=$('#propertiesPanel');if(!p)return;const a=state.adjustments;const slider=(label,key,min,max,step=1)=>`<div class="slider-line"><label>${label}</label><input type="range" min="${min}" max="${max}" step="${step}" value="${a[key]}" data-adj-input="${key}"><input type="number" min="${min}" max="${max}" step="${step}" value="${a[key]}" data-adj-input="${key}"></div>`;let title=labelize(kind),html=`<div style="font-weight:700;margin-bottom:3px">Properties: ${title}</div>`;if(['brightness','levels','curves'].includes(kind))html+=slider('Brightness','brightness',-100,100)+slider('Contrast','contrast',-100,100);else if(kind==='exposure')html+=slider('Exposure','exposure',-100,100)+slider('Gamma','gamma',.1,3,.1);else if(['hue','vibrance'].includes(kind))html+=slider('Hue','hue',-180,180)+slider('Saturation','saturation',0,250);else if(kind==='blackwhite'){a.saturation=0;html+=slider('Brightness','brightness',-100,100)+slider('Contrast','contrast',-100,100);}else if(kind==='photoFilter')html+=slider('Temperature','temperature',-100,100)+slider('Saturation','saturation',0,250);else if(kind==='posterize')html+=slider('Contrast','contrast',-100,100)+slider('Gamma','gamma',.1,3,.1);else html+=slider('Brightness','brightness',-100,100)+slider('Saturation','saturation',0,250);html+=`<div style="display:flex;gap:7px;margin-top:5px"><button class="tiny-btn" data-action="resetAdjustments">Reset</button><button class="tiny-btn" data-action="adjustLayer">New Layer</button></div>`;p.innerHTML=html;p.querySelectorAll('[data-adj-input]').forEach(el=>el.addEventListener('input',e=>{const key=e.target.dataset.adjInput;let val=+e.target.value;if(key==='temperature'){state.adjustments.temperature=val;}else state.adjustments[key]=val;p.querySelectorAll(`[data-adj-input="${key}"]`).forEach(o=>{if(o!==e.target)o.value=val;});renderAll(false);}));actions.resetAdjustments=()=>{state.adjustments={brightness:0,contrast:0,saturation:100,hue:0,exposure:0,gamma:1,temperature:0};renderAll();showProperties(kind);toast('Adjustments reset.');};}
function renderNavigatorPanel(c){c.innerHTML=`<div class="nav-holder"><canvas id="navCanvas" width="220" height="124"></canvas></div><div style="display:flex;gap:7px;justify-content:center"><button class="tiny-btn" data-action="zoomOut">−</button><button class="tiny-btn" data-action="fit">Fit</button><button class="tiny-btn" data-action="zoomIn">+</button></div>`;updateNavigator();}
function renderInfoPanel(c){const l=activeLayer();c.innerHTML=`<div class="info-grid"><strong>Document</strong><span>${escapeHtml(state.docName)}</span><strong>Canvas</strong><span>${mainCanvas.width} × ${mainCanvas.height}</span><strong>Native</strong><span>${state.nativeW} × ${state.nativeH}</span><strong>Zoom</strong><span>${percent(state.zoom)}</span><strong>Tool</strong><span>${toolMap.get(state.activeTool).label}</span><strong>Layer</strong><span>${l?escapeHtml(l.name):'None'}</span><strong>Selections</strong><span>${state.selection?'Active':'None'}</span><strong>Slices</strong><span>${state.slices.length}</span><strong>Paths</strong><span>${state.paths.length}</span></div><div style="margin-top:10px"><button class="tiny-btn" data-action="runAudit">Run UI check</button></div>`;}
function updateHistogram(){const hc=$('#histCanvas');if(!hc)return;const hctx=hc.getContext('2d',{willReadFrequently:true}),w=hc.width,h=hc.height;hctx.fillStyle='#454545';hctx.fillRect(0,0,w,h);let data;try{data=ctx.getImageData(0,0,mainCanvas.width,mainCanvas.height).data;}catch{return;}const br=new Uint32Array(256),bg=new Uint32Array(256),bb=new Uint32Array(256),bl=new Uint32Array(256);const step=Math.max(4,Math.floor(data.length/4/18000));for(let i=0;i<data.length;i+=4*step){const r=data[i],g=data[i+1],b=data[i+2];br[r]++;bg[g]++;bb[b]++;bl[Math.round((r+g+b)/3)]++;}const max=Math.max(...bl,...br,...bg,...bb)||1;draw(bl,'rgba(220,220,220,.55)');draw(br,'rgba(255,80,75,.72)');draw(bg,'rgba(80,255,80,.62)');draw(bb,'rgba(60,110,255,.78)');function draw(bins,col){hctx.beginPath();hctx.moveTo(0,h);for(let i=0;i<256;i++){const x=i/255*w,y=h-Math.pow(bins[i]/max,.45)*h;hctx.lineTo(x,y);}hctx.lineTo(w,h);hctx.closePath();hctx.fillStyle=col;hctx.fill();}}
function updateNavigator(){const nc=$('#navCanvas');if(!nc)return;const nctx=nc.getContext('2d');nctx.fillStyle='#222';nctx.fillRect(0,0,nc.width,nc.height);nctx.drawImage(mainCanvas,0,0,nc.width,nc.height);const wr=workspace.getBoundingClientRect();const vx=-state.panX/state.zoom,vy=-state.panY/state.zoom,vw=wr.width/state.zoom,vh=wr.height/state.zoom;nctx.strokeStyle='#ff2e2e';nctx.lineWidth=1;nctx.strokeRect(vx/mainCanvas.width*nc.width,vy/mainCanvas.height*nc.height,vw/mainCanvas.width*nc.width,vh/mainCanvas.height*nc.height);}
function setTopPanel(p){state.topPanel=p;renderTopPanel();}
function setBottomPanel(p){state.bottomPanel=p;renderBottomPanel();}
function drawStroke(a,b,tool){const layer=ensureEditableLayer(tool);if(!layer)return;const g=layer.canvas.getContext('2d');const size=tool==='pencil'?Math.max(1,state.brushSize*.45):state.brushSize;g.save();clipToSelection(g);g.lineJoin='round';g.lineCap='round';g.lineWidth=size;g.globalAlpha=clamp(state.opacity/100*state.flow/100,.01,1);if(['eraser','backgroundEraser','magicEraser'].includes(tool)){g.globalCompositeOperation='destination-out';g.strokeStyle='rgba(0,0,0,1)';}
else if(tool==='cloneStamp'&&state.cloneSource){const sx=state.cloneSource.x+(b.x-state.start.x)-size/2,sy=state.cloneSource.y+(b.y-state.start.y)-size/2;g.beginPath();g.arc(b.x,b.y,size/2,0,Math.PI*2);g.clip();g.drawImage(state.lastComposite||mainCanvas,sx,sy,size,size,b.x-size/2,b.y-size/2,size,size);g.restore();renderComposite();return;}
else if(['historyBrush','artHistoryBrush'].includes(tool)&&state.initialComposite){g.beginPath();g.arc(b.x,b.y,size/2,0,Math.PI*2);g.clip();g.drawImage(state.initialComposite,b.x-size/2,b.y-size/2,size,size,b.x-size/2,b.y-size/2,size,size);g.restore();renderComposite();return;}
else if(['spotHeal','healingBrush'].includes(tool)){g.strokeStyle='rgba(230,230,230,.35)';g.globalCompositeOperation='soft-light';}
else if(tool==='blur'){g.strokeStyle='rgba(255,255,255,.12)';g.globalCompositeOperation='soft-light';}
else if(tool==='sharpen'){g.strokeStyle='rgba(255,255,255,.18)';g.globalCompositeOperation='overlay';}
else if(tool==='dodge'){g.strokeStyle='rgba(255,255,255,.35)';g.globalCompositeOperation='soft-light';}
else if(tool==='burn'){g.strokeStyle='rgba(0,0,0,.35)';g.globalCompositeOperation='soft-light';}
else if(tool==='sponge'){g.strokeStyle='rgba(128,128,128,.25)';g.globalCompositeOperation='saturation';}
else if(tool==='patternStamp'){g.strokeStyle=makePattern(g,state.fg,state.bg);}
else if(tool==='colorReplacement'){g.strokeStyle=state.fg;g.globalCompositeOperation='color';}
else if(tool==='mixerBrush'){const mix=hexToRgb(state.fg),sample=samplePixel({x:b.x,y:b.y});g.strokeStyle=rgbToHex((mix.r+sample.r)/2,(mix.g+sample.g)/2,(mix.b+sample.b)/2);}
else g.strokeStyle=state.fg;g.beginPath();g.moveTo(a.x,a.y);g.lineTo(b.x,b.y);g.stroke();g.restore();renderComposite();}
function commitShape(a,b,tool){const layer=ensureEditableLayer('shape');if(!layer)return;const g=layer.canvas.getContext('2d'),r=normalizeRect(a,b);g.save();clipToSelection(g);g.globalAlpha=state.opacity/100;g.lineWidth=Math.max(1,state.brushSize/10);g.fillStyle=state.fg;g.strokeStyle=state.bg;if(tool==='rectangle'){if(state.shapeMode!=='stroke')g.fillRect(r.x,r.y,r.w,r.h);if(state.shapeMode!=='fill')g.strokeRect(r.x,r.y,r.w,r.h);}else{if(tool==='ellipse'){g.beginPath();g.ellipse(r.x+r.w/2,r.y+r.h/2,Math.abs(r.w/2),Math.abs(r.h/2),0,0,Math.PI*2);}if(tool==='triangle')trianglePath(g,r);if(tool==='polygon')polygonPath(g,r,6);if(tool==='customShape')starPath(g,r);if(tool==='line'){g.beginPath();g.moveTo(a.x,a.y);g.lineTo(b.x,b.y);}if(tool==='line'||state.shapeMode!=='fill')g.stroke();if(tool!=='line'&&state.shapeMode!=='stroke')g.fill();}g.restore();octx.clearRect(0,0,overlayCanvas.width,overlayCanvas.height);renderAll();}
function commitGradient(a,b){const layer=ensureEditableLayer('gradient');if(!layer)return;const g=layer.canvas.getContext('2d');let grad;if(state.gradientKind==='radial'){const r=Math.max(Math.abs(b.x-a.x),Math.abs(b.y-a.y));grad=g.createRadialGradient(a.x,a.y,0,a.x,a.y,r);}else if(state.gradientKind==='reflected'){grad=g.createLinearGradient(a.x,a.y,b.x,b.y);grad.addColorStop(0,state.bg);grad.addColorStop(.5,state.fg);grad.addColorStop(1,state.bg);}else{grad=g.createLinearGradient(a.x,a.y,b.x,b.y);}if(state.gradientKind!=='reflected'){grad.addColorStop(0,state.fg);grad.addColorStop(1,state.bg);}g.save();clipToSelection(g);g.globalAlpha=state.opacity/100;g.fillStyle=grad;g.fillRect(0,0,mainCanvas.width,mainCanvas.height);g.restore();renderAll();}
function fillSelection(point=null){const layer=ensureEditableLayer('fill');if(!layer)return;const g=layer.canvas.getContext('2d');g.save();g.fillStyle=point&&state.activeTool==='materialDrop'?makePattern(g,state.fg,state.bg):state.fg;g.globalAlpha=state.opacity/100;if(state.selection)fillSelectionPath(g,state.selection);else if(point&&state.activeTool==='paintBucket')floodFillLayer(layer,point,state.fg,state.tolerance);else g.fillRect(0,0,mainCanvas.width,mainCanvas.height);g.restore();renderAll();}
function strokeSelection(){if(!state.selection)return toast('No selection to stroke.');const layer=ensureEditableLayer('stroke');const g=layer.canvas.getContext('2d');g.save();g.strokeStyle=state.fg;g.lineWidth=Math.max(1,state.brushSize/8);strokeSelectionPath(g,state.selection);g.restore();renderAll();}
function clipToSelection(g){if(!state.selection)return;selectionPath(g,state.selection);try{g.clip(state.selection.inverted?'evenodd':'nonzero');}catch{g.clip();}}
function selectionPath(g,s){g.beginPath();if(s.inverted)g.rect(0,0,mainCanvas.width,mainCanvas.height);if(s.type==='rect')g.rect(s.x,s.y,s.w,s.h);else if(s.type==='ellipse')g.ellipse(s.x+s.w/2,s.y+s.h/2,s.w/2,s.h/2,0,0,Math.PI*2);else if(s.type==='path')pathFromPoints(g,s.points,true);}
function fillSelectionPath(g,s){selectionPath(g,s);try{g.fill(s.inverted?'evenodd':'nonzero');}catch{g.fill();}}
function strokeSelectionPath(g,s){if(s.type==='rect')g.strokeRect(s.x,s.y,s.w,s.h);else{selectionPath(g,s);g.stroke();}}
function setSelection(sel){state.previousSelection=copyObj(state.selection);if(state.selectionMode==='add'&&state.selection&&sel.type==='rect'&&state.selection.type==='rect'){const x=Math.min(state.selection.x,sel.x),y=Math.min(state.selection.y,sel.y),r=Math.max(state.selection.x+state.selection.w,sel.x+sel.w),b=Math.max(state.selection.y+state.selection.h,sel.y+sel.h);state.selection={x,y,w:r-x,h:b-y,type:'rect'};}else if(state.selectionMode==='subtract'&&state.selection){state.selection=sel;state.selection.inverted=true;}else if(state.selectionMode==='intersect'&&state.selection&&sel.type==='rect'&&state.selection.type==='rect'){const x=Math.max(state.selection.x,sel.x),y=Math.max(state.selection.y,sel.y),r=Math.min(state.selection.x+state.selection.w,sel.x+sel.w),b=Math.min(state.selection.y+state.selection.h,sel.y+sel.h);state.selection={x,y,w:Math.max(0,r-x),h:Math.max(0,b-y),type:'rect'};}else state.selection=sel;drawOverlay();toast('Selection active.');}
function selectAll(){setSelection({x:0,y:0,w:mainCanvas.width,h:mainCanvas.height,type:'rect'});} function deselect(){state.previousSelection=copyObj(state.selection)||state.previousSelection;state.selection=null;state.crop=null;drawOverlay();toast('Deselected.');} function reselect(){if(state.previousSelection){state.selection=copyObj(state.previousSelection);drawOverlay();toast('Selection restored.');}else toast('No previous selection.');} function invertSelection(){if(!state.selection)return toast('No selection to invert.');state.selection.inverted=!state.selection.inverted;drawOverlay();toast(state.selection.inverted?'Inverse selection active.':'Selection restored.');}
function selectSubject(){const r={x:mainCanvas.width*.18,y:mainCanvas.height*.16,w:mainCanvas.width*.64,h:mainCanvas.height*.62,type:'ellipse'};setSelection(r);toast('Subject selection estimated.');}
function selectAndMask(){showModal('Select and Mask',`<p>Refine the active selection. These controls apply immediately.</p><div class="slider-line"><label>Feather</label><input type="range" min="0" max="80" value="${state.feather}" data-opt="Feather"><input type="number" min="0" max="80" value="${state.feather}" data-opt="Feather"></div><div style="display:flex;gap:8px;margin-top:10px"><button data-action="invertSelection">Invert</button><button data-action="selectSubject">Subject</button><button data-action="selectMaskApply">Apply Mask</button></div>`,[{label:'Cancel',action:'closeModal'},{label:'OK',class:'primary',action:'closeModal'}]);}
function applySelectMask(){addMask();closeModal();}
function setSelectionMode(m){state.selectionMode=m;renderOptions();toast(`Selection mode: ${m}`);}
function magicWandSelect(p){const c=samplePixel(p);const data=ctx.getImageData(0,0,mainCanvas.width,mainCanvas.height).data;let minX=mainCanvas.width,minY=mainCanvas.height,maxX=0,maxY=0,count=0;const step=3,tol=state.tolerance;for(let y=0;y<mainCanvas.height;y+=step){for(let x=0;x<mainCanvas.width;x+=step){const i=(y*mainCanvas.width+x)*4,dist=Math.abs(data[i]-c.r)+Math.abs(data[i+1]-c.g)+Math.abs(data[i+2]-c.b);if(dist<tol*3){minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x);maxY=Math.max(maxY,y);count++;}}}if(count)setSelection({x:minX,y:minY,w:maxX-minX+step,h:maxY-minY+step,type:'rect'});else toast('No similar color found.');}
function copySelection(){const r=selectionBounds()||{x:0,y:0,w:mainCanvas.width,h:mainCanvas.height};const c=document.createElement('canvas');c.width=Math.max(1,Math.round(r.w));c.height=Math.max(1,Math.round(r.h));c.getContext('2d').drawImage(mainCanvas,r.x,r.y,r.w,r.h,0,0,c.width,c.height);state.clipboard=c;toast('Copied to internal clipboard.');}
function cutSelection(){copySelection();const l=ensureEditableLayer('cut');if(!l)return;pushHistory('Cut');const g=l.canvas.getContext('2d');g.save();g.globalCompositeOperation='destination-out';if(state.selection)fillSelectionPath(g,state.selection);else g.fillRect(0,0,l.canvas.width,l.canvas.height);g.restore();renderAll();}
function pasteClipboard(){if(!state.clipboard)return toast('Clipboard empty. Use Copy first.');pushHistory('Paste');const l=createLayer('Pasted Layer');l.canvas.getContext('2d').drawImage(state.clipboard,mainCanvas.width/2-state.clipboard.width/2,mainCanvas.height/2-state.clipboard.height/2);state.layers.push(l);state.activeLayer=state.layers.length-1;renderAll();toast('Pasted as new layer.');}
function selectionBounds(){const s=state.selection;if(!s)return null;if(s.type==='rect'||s.type==='ellipse')return s;if(s.type==='path'){let xs=s.points.map(p=>p.x),ys=s.points.map(p=>p.y);return{x:Math.min(...xs),y:Math.min(...ys),w:Math.max(...xs)-Math.min(...xs),h:Math.max(...ys)-Math.min(...ys)};}return null;}
function applyCrop(){const r=state.crop||state.selection;if(!r)return toast('Draw a crop or selection first.');pushHistory('Crop');const w=Math.max(1,Math.round(r.w)),h=Math.max(1,Math.round(r.h)),oldW=mainCanvas.width,oldH=mainCanvas.height;state.layers.forEach(l=>{const nc=document.createElement('canvas');nc.width=w;nc.height=h;nc.getContext('2d').drawImage(l.canvas,r.x,r.y,r.w,r.h,0,0,w,h);l.canvas=nc;l.offsetX=0;l.offsetY=0;if(l.mask){const nm=document.createElement('canvas');nm.width=w;nm.height=h;nm.getContext('2d').drawImage(l.mask,r.x,r.y,r.w,r.h,0,0,w,h);l.mask=nm;}});mainCanvas.width=overlayCanvas.width=w;mainCanvas.height=overlayCanvas.height=h;state.nativeW=Math.max(1,Math.round(state.nativeW*w/oldW));state.nativeH=Math.max(1,Math.round(state.nativeH*h/oldH));state.crop=null;state.selection=null;fitToScreen(false);renderAll();toast('Crop applied.');}
function cropToSelection(){if(!state.selection)return toast('Make a selection first.');const r=selectionBounds();state.crop=r;applyCrop();}
function rotateCanvas(){pushHistory('Rotate canvas');const oldW=mainCanvas.width,oldH=mainCanvas.height;state.layers.forEach(l=>{l.canvas=rotatedCanvas(l.canvas);if(l.mask)l.mask=rotatedCanvas(l.mask);l.offsetX=0;l.offsetY=0;});mainCanvas.width=overlayCanvas.width=oldH;mainCanvas.height=overlayCanvas.height=oldW;[state.nativeW,state.nativeH]=[state.nativeH,state.nativeW];fitToScreen(false);renderAll();}
function imageSize(){showModal('Image Size',`<p>Current: ${mainCanvas.width} × ${mainCanvas.height}</p><div class="slider-line"><label>Width</label><input id="modalW" type="range" min="100" max="2400" value="${mainCanvas.width}"><input id="modalWn" type="number" min="100" max="2400" value="${mainCanvas.width}"></div><div class="slider-line"><label>Height</label><input id="modalH" type="range" min="100" max="1800" value="${mainCanvas.height}"><input id="modalHn" type="number" min="100" max="1800" value="${mainCanvas.height}"></div>`,[{label:'Cancel',action:'closeModal'},{label:'Resize',class:'primary',action:'applyImageSize'}]);wireModalPairs('modalW','modalWn');wireModalPairs('modalH','modalHn');actions.applyImageSize=()=>{resizeCanvas(+$('#modalWn').value,+$('#modalHn').value);closeModal();};}
function canvasSize(){showModal('Canvas Size',`<p>Add or remove canvas area around layers.</p><div class="slider-line"><label>Width</label><input id="modalCW" type="range" min="100" max="2400" value="${mainCanvas.width}"><input id="modalCWn" type="number" min="100" max="2400" value="${mainCanvas.width}"></div><div class="slider-line"><label>Height</label><input id="modalCH" type="range" min="100" max="1800" value="${mainCanvas.height}"><input id="modalCHn" type="number" min="100" max="1800" value="${mainCanvas.height}"></div>`,[{label:'Cancel',action:'closeModal'},{label:'Apply',class:'primary',action:'applyCanvasSize'}]);wireModalPairs('modalCW','modalCWn');wireModalPairs('modalCH','modalCHn');actions.applyCanvasSize=()=>{changeCanvasSize(+$('#modalCWn').value,+$('#modalCHn').value);closeModal();};}
function resizeCanvas(w,h){pushHistory('Image size');state.layers.forEach(l=>{const nc=document.createElement('canvas');nc.width=w;nc.height=h;nc.getContext('2d').drawImage(l.canvas,0,0,w,h);l.canvas=nc;if(l.mask){const nm=document.createElement('canvas');nm.width=w;nm.height=h;nm.getContext('2d').drawImage(l.mask,0,0,w,h);l.mask=nm;}});mainCanvas.width=overlayCanvas.width=w;mainCanvas.height=overlayCanvas.height=h;state.nativeW=w;state.nativeH=h;fitToScreen(false);renderAll();toast('Image resized.');}
function changeCanvasSize(w,h){pushHistory('Canvas size');state.layers.forEach(l=>{const nc=document.createElement('canvas');nc.width=w;nc.height=h;nc.getContext('2d').drawImage(l.canvas,0,0);l.canvas=nc;if(l.mask){const nm=document.createElement('canvas');nm.width=w;nm.height=h;nm.getContext('2d').drawImage(l.mask,0,0);l.mask=nm;}});mainCanvas.width=overlayCanvas.width=w;mainCanvas.height=overlayCanvas.height=h;state.nativeW=w;state.nativeH=h;fitToScreen(false);renderAll();toast('Canvas size changed.');}
function contentAwareFill(){
pushHistory('Content-Aware Fill');
if(!state.selection){
toast('Make a selection first, then Content-Aware Fill.');
return;
}
const l=ensureEditableLayer('contentAwareFill');
const g=l.canvas.getContext('2d');
const r=selectionBounds();
g.save();
selectionPath(g,state.selection);
try{g.clip(state.selection.inverted?'evenodd':'nonzero');}catch{g.clip();}
g.filter='blur(18px)';
g.drawImage(mainCanvas,Math.max(0,r.x-60),Math.max(0,r.y-60),Math.min(mainCanvas.width,r.w+120),Math.min(mainCanvas.height,r.h+120),r.x,r.y,r.w,r.h);
g.filter='none';
g.restore();
renderAll();
toast('Content-Aware Fill blended the selection.');
}
function applyPixelFilter(type){pushHistory(type);const l=ensureEditableLayer(type);if(!l)return;const g=l.canvas.getContext('2d'),img=g.getImageData(0,0,l.canvas.width,l.canvas.height),d=img.data;for(let i=0;i<d.length;i+=4){if(type==='invert'){d[i]=255-d[i];d[i+1]=255-d[i+1];d[i+2]=255-d[i+2];}if(type==='desaturate'){const y=.299*d[i]+.587*d[i+1]+.114*d[i+2];d[i]=d[i+1]=d[i+2]=y;}}g.putImageData(img,0,0);renderAll();}
function simpleFilter(type){pushHistory(type);const l=ensureEditableLayer(type);if(!l)return;const g=l.canvas.getContext('2d');if(type==='noise'){const img=g.getImageData(0,0,l.canvas.width,l.canvas.height),d=img.data;for(let i=0;i<d.length;i+=4){const n=(Math.random()-.5)*44;d[i]=clamp(d[i]+n,0,255);d[i+1]=clamp(d[i+1]+n,0,255);d[i+2]=clamp(d[i+2]+n,0,255);}g.putImageData(img,0,0);}else if(type==='pixelate'){const small=document.createElement('canvas');small.width=Math.ceil(l.canvas.width/12);small.height=Math.ceil(l.canvas.height/12);small.getContext('2d').drawImage(l.canvas,0,0,small.width,small.height);g.imageSmoothingEnabled=false;g.clearRect(0,0,l.canvas.width,l.canvas.height);g.drawImage(small,0,0,l.canvas.width,l.canvas.height);g.imageSmoothingEnabled=true;}else if(type==='median'){boxBlurCanvas(l.canvas,1);}else{const copy=cloneCanvas(l.canvas);g.clearRect(0,0,l.canvas.width,l.canvas.height);g.filter=type==='blur'?'blur(2px)':'contrast(1.18)';g.drawImage(copy,0,0);g.filter='none';}renderAll();toast(`${labelize(type)} filter applied.`);}
function filterDialog(type){showModal('Gaussian Blur',`<p>Apply blur amount to active layer.</p><div class="slider-line"><label>Radius</label><input id="blurR" type="range" min="1" max="12" value="3"><input id="blurRn" type="number" min="1" max="12" value="3"></div>`,[{label:'Cancel',action:'closeModal'},{label:'Apply',class:'primary',action:'applyGaussian'}]);wireModalPairs('blurR','blurRn');actions.applyGaussian=()=>{const l=ensureEditableLayer('blur');pushHistory('Gaussian blur');const g=l.canvas.getContext('2d'),copy=cloneCanvas(l.canvas);g.clearRect(0,0,l.canvas.width,l.canvas.height);g.filter=`blur(${$('#blurRn').value}px)`;g.drawImage(copy,0,0);g.filter='none';renderAll();closeModal();};}
function autoTone(){state.adjustments.brightness=8;state.adjustments.contrast=14;state.adjustments.saturation=112;renderAll();showProperties('brightness');toast('Auto Tone applied.');}
function autoContrast(){state.adjustments.contrast=22;renderAll();showProperties('brightness');toast('Auto Contrast applied.');}
function autoColor(){state.adjustments.temperature=8;state.adjustments.saturation=112;renderAll();showProperties('photoFilter');toast('Auto Color applied.');}
function sampleColor(p,marker=false){const c=samplePixel(p);state.fg=rgbToHex(c.r,c.g,c.b);syncColorInputs();renderOptions();if(marker){state.markers.push({type:'sample',x:p.x,y:p.y,label:String(state.markers.filter(m=>m.type==='sample').length+1),color:state.fg});drawOverlay();}toast(`Sampled ${state.fg.toUpperCase()}`);}
function addNoteAt(p){const text=prompt('Note text','Note');if(!text)return;state.notes.push({x:p.x,y:p.y,text});drawOverlay();toast('Note added.');}
function patchAt(p){const l=ensureEditableLayer('patch');const g=l.canvas.getContext('2d');const r=state.selection?selectionBounds():{x:p.x-state.brushSize,y:p.y-state.brushSize,w:state.brushSize*2,h:state.brushSize*2};g.save();g.filter='blur(8px)';g.drawImage(mainCanvas,r.x-20,r.y-20,r.w+40,r.h+40,r.x,r.y,r.w,r.h);g.filter='none';g.restore();renderAll();toast('Patch blended.');}
function contentMoveAt(p){if(!state.selection)return toast('Select an area first.');const r=selectionBounds(),l=ensureEditableLayer('contentMove'),g=l.canvas.getContext('2d');const temp=document.createElement('canvas');temp.width=r.w;temp.height=r.h;temp.getContext('2d').drawImage(mainCanvas,r.x,r.y,r.w,r.h,0,0,r.w,r.h);g.clearRect(r.x,r.y,r.w,r.h);g.drawImage(temp,p.x-r.w/2,p.y-r.h/2);renderAll();toast('Content moved.');}
function redEyeFix(p){const l=ensureEditableLayer('redEye'),g=l.canvas.getContext('2d'),r=Math.round(state.brushSize);const x=Math.max(0,Math.round(p.x-r)),y=Math.max(0,Math.round(p.y-r)),w=Math.min(l.canvas.width-x,r*2),h=Math.min(l.canvas.height-y,r*2),img=g.getImageData(x,y,w,h),d=img.data;for(let i=0;i<d.length;i+=4){if(d[i]>d[i+1]*1.25&&d[i]>d[i+2]*1.25){const yv=.35*d[i]+.35*d[i+1]+.3*d[i+2];d[i]=d[i+1]=d[i+2]=yv*.55;}}g.putImageData(img,x,y);renderAll();toast('Red eye reduced.');}
function handlePathDown(p,tool,e){pushHistory('Path');if(tool==='pathSelection'||tool==='directSelection'){selectNearestPath(p);state.drawing=false;return;}if(tool==='deleteAnchor'){deleteNearestAnchor(p);state.drawing=false;return;}if(tool==='addAnchor'){addAnchorToActivePath(p);state.drawing=false;return;}if(tool==='pen'||tool==='curvaturePen'){if(state.activePath<0||!state.paths[state.activePath]){state.paths.push({name:'Work Path '+(state.paths.length+1),points:[],closed:false});state.activePath=state.paths.length-1;}state.paths[state.activePath].points.push(p);drawOverlay();state.drawing=false;toast('Anchor added. Double click Make Selection in Paths panel when done.');return;}state.points=[p];}
function handlePathUp(p,tool){if(['freeformPen','curvaturePen'].includes(tool)&&state.points.length>1){state.paths.push({name:'Work Path '+(state.paths.length+1),points:state.points.slice(),closed:false});state.activePath=state.paths.length-1;drawOverlay();renderBottomPanel();toast('Path created.');}}
function makeSelectionFromPath(){const p=state.paths[state.activePath];if(!p)return toast('No active path.');setSelection({type:'path',points:p.points.slice()});}
function deleteActivePath(){if(state.activePath<0)return toast('No active path.');state.paths.splice(state.activePath,1);state.activePath=Math.min(state.activePath,state.paths.length-1);drawOverlay();renderBottomPanel();toast('Path deleted.');}
function selectNearestPath(p){let best=-1,dist=Infinity;state.paths.forEach((path,i)=>path.points.forEach(pt=>{const d=(pt.x-p.x)**2+(pt.y-p.y)**2;if(d<dist){dist=d;best=i;}}));if(best>=0){state.activePath=best;renderBottomPanel();drawOverlay();toast('Path selected.');}}
function addAnchorToActivePath(p){if(state.activePath<0){state.paths.push({name:'Work Path '+(state.paths.length+1),points:[p],closed:false});state.activePath=0;}else state.paths[state.activePath].points.push(p);drawOverlay();renderBottomPanel();toast('Anchor added.');}
function deleteNearestAnchor(p){if(state.activePath<0)return toast('No active path.');const path=state.paths[state.activePath];let best=-1,dist=Infinity;path.points.forEach((pt,i)=>{const d=(pt.x-p.x)**2+(pt.y-p.y)**2;if(d<dist){dist=d;best=i;}});if(best>=0){path.points.splice(best,1);drawOverlay();renderBottomPanel();toast('Anchor deleted.');}}
function selectSliceAt(p){state.activeSlice=state.slices.findIndex(s=>p.x>=s.x&&p.x<=s.x+s.w&&p.y>=s.y&&p.y<=s.y+s.h);drawOverlay();toast(state.activeSlice>=0?'Slice selected.':'No slice under pointer.');}
function newDoc(){pushHistory('New document');mainCanvas.width=overlayCanvas.width=1200;mainCanvas.height=overlayCanvas.height=675;state.docName='Untitled-1';state.nativeW=1200;state.nativeH=675;state.ppi=240;state.layers=[createLayer('Background',{locked:true,draw:g=>{g.fillStyle='#fff';g.fillRect(0,0,mainCanvas.width,mainCanvas.height);}})];state.activeLayer=0;state.selection=state.crop=null;state.slices=[];state.markers=[];state.notes=[];state.paths=[];state.artboards=[];state.adjustments={brightness:0,contrast:0,saturation:100,hue:0,exposure:0,gamma:1,temperature:0};$('#docName').textContent=state.docName;fitToScreen(false);renderAll();toast('New document created.');}
function duplicateDoc(){pushHistory('Duplicate document');state.layers=state.layers.map(l=>{const n=createLayer(l.name,{visible:l.visible,locked:false,opacity:l.opacity,fillOpacity:l.fillOpacity,blend:l.blend,offsetX:l.offsetX,offsetY:l.offsetY,kind:l.kind,effects:l.effects});n.canvas.getContext('2d').drawImage(l.canvas,0,0);if(l.mask)n.mask=cloneCanvas(l.mask);return n;});state.docName=state.docName.replace(/(\.[^.]+)?$/,' copy$1');$('#docName').textContent=state.docName;renderAll();toast('Document duplicated.');}
function closeDoc(){openSample(true);toast('Document closed. Sample reopened so the editor stays usable.');}
function onOpenFile(e){const file=e.target.files&&e.target.files[0];if(!file)return;const reader=new FileReader();reader.onload=()=>{const img=new Image();img.onload=()=>{pushHistory('Open image');const maxDim=1800,scale=Math.min(1,maxDim/Math.max(img.naturalWidth,img.naturalHeight));mainCanvas.width=overlayCanvas.width=Math.max(1,Math.round(img.naturalWidth*scale));mainCanvas.height=overlayCanvas.height=Math.max(1,Math.round(img.naturalHeight*scale));state.docName=file.name;state.nativeW=img.naturalWidth;state.nativeH=img.naturalHeight;state.layers=[createLayer('Background',{locked:true,draw:g=>g.drawImage(img,0,0,mainCanvas.width,mainCanvas.height)})];state.activeLayer=0;state.selection=state.crop=null;state.slices=[];state.markers=[];state.notes=[];state.paths=[];$('#docName').textContent=file.name;fitToScreen(false);renderAll();state.initialComposite=cloneCanvas(mainCanvas);e.target.value='';};img.src=reader.result;};reader.readAsDataURL(file);}
function exportImage(kind){renderComposite();const a=document.createElement('a');const ext=kind==='jpeg'?'jpg':'png';a.download=state.docName.replace(/\.[^.]+$/,'')+'_edited.'+ext;a.href=mainCanvas.toDataURL(kind==='jpeg'?'image/jpeg':'image/png',.92);a.click();toast(`${ext.toUpperCase()} exported.`);}
function setTopAndBottomHidden(hidden){$('#rightPanels').style.display=hidden?'none':'grid';}
function togglePanels(){const hidden=$('#rightPanels').style.display==='none';setTopAndBottomHidden(hidden);toast(hidden?'Panels shown.':'Panels hidden.');}
function workspaceMenu(){showModal('Workspace',`<p>Choose a workspace preset.</p><div style="display:flex;gap:8px;flex-wrap:wrap"><button data-action="wsPhotography">Photography</button><button data-action="wsPainting">Painting</button><button data-action="wsDesign">Design</button><button data-action="workspaceReset">Reset</button></div>`,[{label:'Done',class:'primary',action:'closeModal'}]);actions.wsPhotography=()=>{setTool('move');setTopPanel('histogram');setBottomPanel('layers');toast('Photography workspace.');};actions.wsPainting=()=>{setTool('brush');toast('Painting workspace.');};actions.wsDesign=()=>{setTool('rectangle');toast('Design workspace.');};}
function commandPalette(){const rows=[['New Document','newDoc'],['Open','open'],['Export PNG','exportPng'],['Brush Tool','tool:brush'],['Crop Tool','tool:crop'],['Run UI Check','runAudit']];showModal('Search Commands',`<input id="cmdSearch" style="width:100%;height:28px" placeholder="Search commands"><div id="cmdResults" style="margin-top:10px"></div>`,[{label:'Close',action:'closeModal'}]);const render=()=>{$('#cmdResults').innerHTML=rows.filter(r=>r[0].toLowerCase().includes(($('#cmdSearch').value||'').toLowerCase())).map(r=>`<button style="display:block;width:100%;text-align:left;padding:7px;border-bottom:1px solid #ccc;color:#111" data-action="cmdRun" data-cmd="${r[1]}">${r[0]}</button>`).join('');};actions.cmdRun=e=>{const cmd=e.target.dataset.cmd;if(cmd.startsWith('tool:'))setTool(cmd.slice(5));else runAction(cmd);closeModal();};$('#cmdSearch').addEventListener('input',render);render();}
function panelMenu(){showModal('Panel Menu',`<button data-action="panelHistogram">Histogram</button> <button data-action="panelNavigator">Navigator</button> <button data-action="panelInfo">Info</button> <button data-action="workspaceReset">Reset panels</button>`,[{label:'Done',class:'primary',action:'closeModal'}]);}
function layerPanelMenu(){showModal('Layer Panel Menu',`<button data-action="newLayer">New Layer</button> <button data-action="duplicateLayer">Duplicate</button> <button data-action="mergeVisible">Merge Visible</button> <button data-action="flatten">Flatten</button>`,[{label:'Done',class:'primary',action:'closeModal'}]);}
function workspaceReset(){state.topPanel='histogram';state.bottomPanel='layers';$('#rightPanels').style.display='grid';fitToScreen(false);renderAll();toast('Workspace reset.');}
function preferences(){showModal('Preferences',`<p>Editor preferences are stored in this page session.</p><label><input type="checkbox" ${state.snap?'checked':''} data-pref="snap"> Snap</label><br><label><input type="checkbox" ${state.grid?'checked':''} data-pref="grid"> Grid</label><br><label><input type="checkbox" ${state.guides?'checked':''} data-pref="guides"> Extras</label>`,[{label:'Done',class:'primary',action:'closeModal'}]);$$('[data-pref]').forEach(el=>el.addEventListener('change',()=>{state[el.dataset.pref]=el.checked;drawOverlay();}));}
function showShortcuts(){const rows=[['Move','V'],['Marquee tools','M / Shift+M'],['Lasso tools','L / Shift+L'],['Selection tools','W / Shift+W'],['Crop tools','C / Shift+C'],['Eyedropper tools','I / Shift+I'],['Healing tools','J / Shift+J'],['Brush tools','B / Shift+B'],['Clone Stamp','S / Shift+S'],['History Brush','Y / Shift+Y'],['Eraser','E / Shift+E'],['Gradient/Paint Bucket','G / Shift+G'],['Blur/Sharpen/Smudge','R / Shift+R'],['Dodge/Burn/Sponge','O / Shift+O'],['Pen','P / Shift+P'],['Type','T / Shift+T'],['Path Selection','A / Shift+A'],['Shapes','U / Shift+U'],['Hand','H or Space'],['Zoom','Z'],['Undo/Redo','Ctrl/⌘+Z, Shift+Ctrl/⌘+Z'],['Save','Ctrl/⌘+S'],['Open','Ctrl/⌘+O'],['Select All/Deselect','Ctrl/⌘+A / Ctrl/⌘+D'],['Brush size','[ and ]'],['Swap/Default Colors','X / D']];showModal('Keyboard Shortcuts',`<table><thead><tr><th>Command</th><th>Shortcut</th></tr></thead><tbody>${rows.map(r=>`<tr><td>${r[0]}</td><td>${r[1].split(' / ').map(k=>`<span class="kbd">${escapeHtml(k)}</span>`).join(' ')}</td></tr>`).join('')}</tbody></table>`,[{label:'OK',class:'primary',action:'closeModal'}]);}
function about(){showModal('About This Build',`<p>This is a single-file HTML/CSS/JS photo editor with Photoshop-style workspace behavior. It uses browser canvas APIs and no proprietary Adobe code or assets.</p><p><strong>UI audit:</strong> ${auditUI().missing.length} missing button handlers.</p>`,[{label:'OK',class:'primary',action:'closeModal'}]);}
function runAudit(){const a=auditUI();toast(a.missing.length?`UI check found ${a.missing.length} missing controls.`:'UI check passed: 0 dead buttons.');if(a.missing.length)console.warn(a.missing);}
function auditUI(){const missing=[];$$('button,.drop-row').forEach(el=>{const hidden=el.offsetParent===null&&el.closest('.dropdown')===null&&el.id!=='modalBackdrop';if(hidden)return;if(!el.dataset.action&&!el.dataset.tool&&!el.dataset.adjust)missing.push((el.textContent||el.title||el.className).trim());});return{missing};}
window.__uiAudit=auditUI;
window.__editorTest={setTool,tools:[...toolMap.keys()],audit:auditUI,actions:()=>Object.keys(actions)};
function showModal(title,body,buttons=[]){$('#modalTitle').textContent=title;$('#modalBody').innerHTML=body;$('#modalActions').innerHTML=(buttons.length?buttons:[{label:'OK',class:'primary',action:'closeModal'}]).map(b=>`<button class="${b.class||''}" data-action="${b.action||'closeModal'}">${escapeHtml(b.label)}</button>`).join('');$('#modalBackdrop').style.display='flex';}
function closeModal(){$('#modalBackdrop').style.display='none';}
function wireModalPairs(a,b){const r=$('#'+a),n=$('#'+b);if(!r||!n)return;r.addEventListener('input',()=>n.value=r.value);n.addEventListener('input',()=>r.value=n.value);}
function openToolFlyout(group){showModal(group.key+' Tool Group',`<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:8px">${group.tools.map(t=>`<button style="text-align:left;padding:8px;border:1px solid #aaa;color:#111;background:#fafafa" data-tool="${t[0]}">${t[2]} ${t[1]}</button>`).join('')}</div>`,[{label:'Close',action:'closeModal'}]);}
function openBrushPreset(){showModal('Brush Preset',`<div style="display:flex;gap:8px;flex-wrap:wrap"><button data-action="brushSmall">Small Soft</button><button data-action="brushMedium">Medium Hard</button><button data-action="brushLarge">Large Soft</button></div>`,[{label:'Done',class:'primary',action:'closeModal'}]);actions.brushSmall=()=>{state.brushSize=12;state.hardness=20;renderOptions();toast('Small soft brush.');};actions.brushMedium=()=>{state.brushSize=38;state.hardness=85;renderOptions();toast('Medium hard brush.');};actions.brushLarge=()=>{state.brushSize=96;state.hardness=30;renderOptions();toast('Large soft brush.');};}
function onWheel(e){e.preventDefault();if(e.ctrlKey||e.metaKey||e.altKey)zoomAtPoint(e.deltaY<0?1.12:.88,e.clientX,e.clientY);else{state.panX-=e.deltaX;state.panY-=e.deltaY;updateStageTransform();updateNavigator();}}
function onKeyDown(e){const tag=e.target?.tagName,typing=['INPUT','TEXTAREA','SELECT'].includes(tag);const key=e.key.toLowerCase(),mod=e.ctrlKey||e.metaKey;if(typing&&!(mod&&['s','o','n','z','j','d','a','i','+','-','=','0','1','c','x','v','w','r'].includes(key)))return;if(key===' '&&!state.tempTool){e.preventDefault();state.tempTool='hand';updateCursor();return;}if(e.altKey&&e.shiftKey&&mod&&key==='k'){e.preventDefault();showShortcuts();return;}if(mod){if(key==='n'){e.preventDefault();newDoc();return;}if(key==='o'){e.preventDefault();$('#fileInput').click();return;}if(key==='s'){e.preventDefault();exportImage('png');return;}if(key==='z'){e.preventDefault();e.shiftKey?redo():undo();return;}if(key==='j'){e.preventDefault();duplicateLayer();return;}if(key==='a'){e.preventDefault();selectAll();return;}if(key==='d'){e.preventDefault();deselect();return;}if(key==='i'){e.preventDefault();e.shiftKey?invertSelection():applyPixelFilter('invert');return;}if(key==='c'){e.preventDefault();copySelection();return;}if(key==='x'){e.preventDefault();cutSelection();return;}if(key==='v'){e.preventDefault();pasteClipboard();return;}if(key==='r'){e.preventDefault();toggleFlag('rulers','Rulers');return;}if(key==='w'){e.preventDefault();closeDoc();return;}if(key==='0'){e.preventDefault();fitToScreen();return;}if(key==='1'){e.preventDefault();setZoom(1,true);return;}if(key==='+'||key==='='){e.preventDefault();zoomAtCenter(1.25);return;}if(key==='-'){e.preventDefault();zoomAtCenter(.8);return;}}if(key==='escape'){deselect();closeModal();return;}if(key==='enter'){if(state.crop)applyCrop();return;}if(key==='['){state.brushSize=Math.max(1,state.brushSize-5);renderOptions();toast(`Brush ${state.brushSize}px`);return;}if(key===']'){state.brushSize=Math.min(1000,state.brushSize+5);renderOptions();toast(`Brush ${state.brushSize}px`);return;}if(key==='x'){swapColors();return;}if(key==='d'){defaultColors();return;}if(key==='f'){cycleScreenMode();return;}if(keyToGroup.has(key)){e.preventDefault();const g=keyToGroup.get(key);e.shiftKey?cycleToolGroup(g):setTool(g.tools[0][0]);return;}}
function onKeyUp(e){if(e.key===' '){state.tempTool=null;updateCursor();}}
function updateCursor(){const t=state.tempTool||state.activeTool;let c='crosshair';if(t==='move')c='move';if(t==='hand')c='grab';if(t==='zoom')c='zoom-in';if(['type','verticalType','typeMask'].includes(t))c='text';if(['eyedropper','colorSampler'].includes(t))c='copy';overlayCanvas.style.cursor=c;}
function alignLayer(pos){const l=activeLayer();if(!l||l.locked)return toast('No editable layer to align.');pushHistory('Align layer');if(pos==='left')l.offsetX=0;if(pos==='center')l.offsetX=0;if(pos==='right')l.offsetX=0;if(pos==='top')l.offsetY=0;if(pos==='middle')l.offsetY=0;if(pos==='bottom')l.offsetY=0;renderAll();toast(`Aligned ${pos}.`);}
function setGradientKind(k){state.gradientKind=k;renderOptions();toast(`${labelize(k)} gradient.`);} function setTextAlign(a){state.textAlign=a;renderOptions();toast(`Text align ${a}.`);} function setShapeMode(m){state.shapeMode=m;renderOptions();toast(`Shape mode ${m}.`);}
function swapColors(){const t=state.fg;state.fg=state.bg;state.bg=t;syncColorInputs();renderOptions();toast('Foreground/background swapped.');} function defaultColors(){state.fg='#ffffff';state.bg='#000000';syncColorInputs();renderOptions();toast('Default colors restored.');} function syncColorInputs(){$('#fgColor').value=state.fg;$('#bgColor').value=state.bg;}
function quickMask(){state.quickMask=!state.quickMask;workspace.classList.toggle('checkered',state.quickMask);toast(state.quickMask?'Quick Mask view on.':'Quick Mask view off.');}
function setScreenMode(m){state.screenMode=m;$$('.screen-mode button').forEach(b=>b.classList.toggle('active',b.dataset.action===(m==='standard'?'standardScreen':'fullScreen')));document.body.style.background=m==='full'?'#000':'#202020';toast(`${labelize(m)} screen mode.`);} function cycleScreenMode(){setScreenMode(state.screenMode==='standard'?'full':'standard');}
function toggleFlag(key,label){state[key]=!state[key];drawOverlay();toast(`${label} ${state[key]?'on':'off'}.`);}
function applyContext(){if(state.crop)applyCrop();else if(state.selection)deselect();else toast('Applied.');}
function new3DLayer(){pushHistory('3D layer');const l=createLayer('3D Extrusion '+state.layers.length,{draw:g=>{g.fillStyle=state.fg;g.shadowColor='rgba(0,0,0,.5)';g.shadowBlur=18;g.shadowOffsetX=18;g.shadowOffsetY=18;g.font='bold 90px Arial';g.fillText('3D',mainCanvas.width*.38,mainCanvas.height*.48);},kind:'shape'});l.effects.shadow=true;state.layers.push(l);state.activeLayer=state.layers.length-1;renderAll();toast('3D-style extrusion layer created.');}
function rotate3D(){state.rotation=(state.rotation+12)%360;updateStageTransform();toast('3D view rotated.');} function lighting3D(){state.adjustments.exposure=clamp(state.adjustments.exposure+8,-100,100);renderAll();showProperties('exposure');toast('Environment lighting increased.');}
function channelView(channel){pushHistory('Channel view');const l=createLayer(labelize(channel)+' channel view',{draw:g=>{g.drawImage(mainCanvas,0,0);const img=g.getImageData(0,0,mainCanvas.width,mainCanvas.height),d=img.data;for(let i=0;i<d.length;i+=4){const v=channel==='red'?d[i]:channel==='green'?d[i+1]:d[i+2];d[i]=d[i+1]=d[i+2]=v;}g.putImageData(img,0,0);}});state.layers.push(l);state.activeLayer=state.layers.length-1;renderAll();toast(`${labelize(channel)} channel layer created.`);}
function addTextAt(x,y,text=null,tool='type'){text=text||prompt('Enter text','Your text');if(!text)return;pushHistory('Type');const l=createLayer((tool==='typeMask'?'Type Mask: ':'Text: ')+text.slice(0,18),{kind:'text'});const g=l.canvas.getContext('2d');g.save();g.fillStyle=state.fg;g.font=`${state.fontSize}px ${cssFont(state.fontFamily)}`;g.textAlign=state.textAlign;g.textBaseline='top';g.shadowColor='rgba(0,0,0,.45)';g.shadowBlur=4;if(tool==='verticalType'){[...text].forEach((ch,i)=>g.fillText(ch,x,y+i*state.fontSize*.82));}else if(tool==='typeMask'){g.globalCompositeOperation='source-over';g.fillText(text,x,y);l.mask=cloneCanvas(l.canvas);}else g.fillText(text,x,y);g.restore();state.layers.push(l);state.activeLayer=state.layers.length-1;renderAll();}
function makePattern(g,fg,bg){const p=document.createElement('canvas');p.width=14;p.height=14;const pg=p.getContext('2d');pg.fillStyle=bg;pg.fillRect(0,0,14,14);pg.fillStyle=fg;pg.fillRect(0,0,7,7);pg.fillRect(7,7,7,7);return g.createPattern(p,'repeat');}
function floodFillLayer(layer,p,color,tol){const g=layer.canvas.getContext('2d'),img=g.getImageData(0,0,layer.canvas.width,layer.canvas.height),d=img.data,w=img.width,h=img.height,x0=Math.floor(p.x),y0=Math.floor(p.y);const target=[d[(y0*w+x0)*4],d[(y0*w+x0)*4+1],d[(y0*w+x0)*4+2],d[(y0*w+x0)*4+3]],fill=hexToRgb(color),stack=[[x0,y0]],seen=new Uint8Array(w*h);let n=0;while(stack.length&&n<90000){const [x,y]=stack.pop();if(x<0||y<0||x>=w||y>=h)continue;const idx=y*w+x;if(seen[idx])continue;seen[idx]=1;const i=idx*4;if(Math.abs(d[i]-target[0])+Math.abs(d[i+1]-target[1])+Math.abs(d[i+2]-target[2])>tol*3)continue;d[i]=fill.r;d[i+1]=fill.g;d[i+2]=fill.b;d[i+3]=255;n++;stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]);}g.putImageData(img,0,0);toast(`Filled ${n.toLocaleString()} pixels.`);}
function boxBlurCanvas(c,passes=1){const g=c.getContext('2d');for(let p=0;p<passes;p++){const img=g.getImageData(0,0,c.width,c.height),d=img.data,copy=new Uint8ClampedArray(d);for(let y=1;y<c.height-1;y++)for(let x=1;x<c.width-1;x++){let r=0,gr=0,b=0,a=0;for(let yy=-1;yy<=1;yy++)for(let xx=-1;xx<=1;xx++){const i=((y+yy)*c.width+x+xx)*4;r+=copy[i];gr+=copy[i+1];b+=copy[i+2];a+=copy[i+3];}const i=(y*c.width+x)*4;d[i]=r/9;d[i+1]=gr/9;d[i+2]=b/9;d[i+3]=a/9;}g.putImageData(img,0,0);}}
function rotatedCanvas(c){const nc=document.createElement('canvas');nc.width=c.height;nc.height=c.width;const g=nc.getContext('2d');g.translate(nc.width/2,nc.height/2);g.rotate(Math.PI/2);g.drawImage(c,-c.width/2,-c.height/2);return nc;}
function cloneCanvas(c){const n=document.createElement('canvas');n.width=c.width;n.height=c.height;n.getContext('2d').drawImage(c,0,0);return n;}
function trianglePath(g,r){g.beginPath();g.moveTo(r.x+r.w/2,r.y);g.lineTo(r.x+r.w,r.y+r.h);g.lineTo(r.x,r.y+r.h);g.closePath();}
function polygonPath(g,r,n=6){const cx=r.x+r.w/2,cy=r.y+r.h/2,rad=Math.min(Math.abs(r.w),Math.abs(r.h))/2;g.beginPath();for(let i=0;i<n;i++){const a=-Math.PI/2+i*Math.PI*2/n,x=cx+Math.cos(a)*rad,y=cy+Math.sin(a)*rad;if(i)g.lineTo(x,y);else g.moveTo(x,y);}g.closePath();}
function starPath(g,r){const cx=r.x+r.w/2,cy=r.y+r.h/2,rad=Math.min(Math.abs(r.w),Math.abs(r.h))/2;g.beginPath();for(let i=0;i<10;i++){const rr=i%2?rad*.45:rad,a=-Math.PI/2+i*Math.PI/5,x=cx+Math.cos(a)*rr,y=cy+Math.sin(a)*rr;if(i)g.lineTo(x,y);else g.moveTo(x,y);}g.closePath();}
function normalizeRect(a,b){return{x:Math.min(a.x,b.x),y:Math.min(a.y,b.y),w:Math.abs(b.x-a.x),h:Math.abs(b.y-a.y)};} function distance(m){return Math.hypot(m.x2-m.x1,m.y2-m.y1);} function samplePixel(p){const d=ctx.getImageData(Math.round(p.x),Math.round(p.y),1,1).data;return{r:d[0],g:d[1],b:d[2],a:d[3]};}
function hexToRgb(hex){hex=hex.replace('#','');return{r:parseInt(hex.slice(0,2),16),g:parseInt(hex.slice(2,4),16),b:parseInt(hex.slice(4,6),16)};} function rgbToHex(r,g,b){return'#'+[r,g,b].map(v=>Math.round(clamp(v,0,255)).toString(16).padStart(2,'0')).join('');}
function wrapText(g,text,x,y,maxWidth,lineHeight){const words=String(text).split(/\s+/);let line='';for(const word of words){const test=line+word+' ';if(g.measureText(test).width>maxWidth&&line){g.fillText(line,x,y);line=word+' ';y+=lineHeight;}else line=test;}g.fillText(line,x,y);} function cssFont(f){return /\s/.test(f)?`"${f}"`:f;}
function toast(message){
const box=$('#toast');
if(!box)return;
box.textContent=message;
box.classList.add('show');
clearTimeout(toast._timer);
toast._timer=setTimeout(()=>box.classList.remove('show'),2300);
}
function labelize(s){return String(s).replace(/([A-Z])/g,' $1').replace(/^./,c=>c.toUpperCase());} function percent(v){return `${(v*100).toFixed(1)}%`;} function clamp(v,min,max){return Math.max(min,Math.min(max,v));} function escapeHtml(s){return String(s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));} function copyObj(o){return o==null?o:JSON.parse(JSON.stringify(o));}
init();
})();
</script>
</body>
</html>
</body>
</html>