anycoder-0825ea5b / index.html
00face's picture
Upload folder using huggingface_hub
1dee760 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Perchance // Img.Edit Pro</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;700;800&display=swap"
rel="stylesheet">
<!-- Icons -->
<script src="https://unpkg.com/@phosphor-icons/web@2.0.0/dist/phosphor.js"></script>
<style>
:root {
--bg: #0c0d10;
--panel: #13151a;
--panel2: #1a1d24;
--border: #2a2d38;
--accent: #7c6ff7;
--accent2: #f7a26f;
--green: #5de8a0;
--red: #f76f6f;
--text: #e2dff8;
--muted: #6b6e82;
--radius: 6px;
--mono: 'DM Mono', 'Courier New', monospace;
--display: 'Syne', sans-serif;
--font-size: 12px;
}
* {
box-sizing: border-box;
outline: none;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: var(--font-size);
height: 100vh;
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--panel);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--panel);
border-bottom: 1px solid var(--border);
height: 50px;
z-index: 10;
}
.logo {
font-family: var(--display);
font-weight: 800;
font-size: 17px;
letter-spacing: -0.5px;
color: var(--text);
display: flex;
align-items: center;
gap: 10px;
}
.logo span {
color: var(--accent);
}
.anycoder-link {
font-size: 10px;
color: var(--muted);
text-decoration: none;
border: 1px solid var(--border);
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
}
.anycoder-link:hover {
color: var(--accent);
border-color: var(--accent);
}
.header-actions {
display: flex;
gap: 8px;
}
button {
font-family: var(--mono);
font-size: 11px;
cursor: pointer;
border: none;
border-radius: var(--radius);
transition: all 0.15s;
background: var(--panel2);
color: var(--text);
border: 1px solid var(--border);
padding: 6px 13px;
display: flex;
align-items: center;
gap: 6px;
}
button:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--panel);
}
button:disabled {
opacity: 0.4;
cursor: default;
}
.btn-primary {
background: var(--accent);
color: #fff;
border: 1px solid transparent;
font-weight: 500;
}
.btn-primary:hover {
background: #9187fa;
border-color: #9187fa;
color: #fff;
}
.btn-danger {
background: var(--red);
color: #fff;
border: 1px solid transparent;
}
.btn-danger:hover {
background: #e85555;
}
.workspace {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}
.panel-left {
width: 220px;
flex-shrink: 0;
background: var(--panel);
border-right: 1px solid var(--border);
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 12px;
}
.label-caps {
font-size: 9px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--muted);
font-family: var(--display);
font-weight: 700;
margin-bottom: 8px;
margin-top: 12px;
}
.tool-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px;
background: var(--panel2);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--muted);
font-size: 9px;
cursor: pointer;
height: 55px;
font-family: var(--mono);
}
.tool-btn:hover {
border-color: var(--accent);
color: var(--text);
background: var(--panel);
}
.tool-btn.active {
background: #1e1b3a;
border-color: var(--accent);
color: var(--accent);
}
.tool-btn svg, .tool-btn i {
width: 18px;
height: 18px;
}
.canvas-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
background: radial-gradient(ellipse at 20% 20%, rgba(124, 111, 247, .06) 0%, transparent 50%), radial-gradient(ellipse at 80% 80%, rgba(247, 162, 111, .04) 0%, transparent 50%), var(--bg);
background-image: linear-gradient(45deg, #13151a 25%, transparent 25%, transparent 75%, #13151a 75%, #13151a), linear-gradient(45deg, #13151a 25%, transparent 25%, transparent 75%, #13151a 75%, #13151a);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.canvas-centerer {
margin: auto;
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: auto;
}
.canvas-wrap {
position: relative;
box-shadow: 0 0 0 1px var(--border), 0 20px 60px rgba(0, 0, 0, .5);
border-radius: 2px;
transition: transform 0.1s;
background: transparent;
transform-origin: center center;
}
canvas {
display: block;
}
.panel-right {
width: 320px;
flex-shrink: 0;
background: var(--panel);
border-left: 1px solid var(--border);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tab-row {
display: flex;
border-bottom: 1px solid var(--border);
overflow-x: auto;
background: var(--panel2);
}
.tab {
flex: 1;
padding: 10px 5px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--muted);
cursor: pointer;
border: none;
background: transparent;
border-bottom: 2px solid transparent;
font-family: var(--mono);
transition: all 0.15s;
white-space: nowrap;
position: relative;
}
.tab:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.02);
}
.tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-panel {
display: none;
padding: 15px;
}
.tab-panel.active {
display: block;
}
.frow {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
.flabel {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
}
.flabel span:first-child {
color: var(--muted);
}
.fval {
color: var(--accent);
min-width: 30px;
text-align: right;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 3px;
border-radius: 2px;
background: var(--border);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]:hover::-webkit-slider-thumb {
transform: scale(1.2);
}
input[type="color"] {
width: 100%;
height: 30px;
border: none;
background: transparent;
cursor: pointer;
}
select {
width: 100%;
background: var(--panel2);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 6px 7px;
font-size: 10px;
font-family: var(--mono);
outline: none;
cursor: pointer;
margin-bottom: 9px;
}
.ai-card {
background: var(--panel2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px;
margin-bottom: 10px;
}
.ai-card p {
font-size: 10px;
color: var(--muted);
line-height: 1.5;
margin: 0;
}
.model-info {
font-size: 9px;
color: var(--accent2);
margin-top: 4px;
font-style: italic;
}
.layer-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.layer-item {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel2);
padding: 6px;
border-radius: 4px;
border: 1px solid var(--border);
position: relative;
cursor: pointer;
}
.layer-item.active {
border-color: var(--accent);
background: #1e1b3a;
}
.layer-thumb {
width: 32px;
height: 32px;
background: #000;
object-fit: cover;
border: 1px solid var(--border);
border-radius: 2px;
}
.layer-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.layer-name {
font-size: 10px;
font-weight: 500;
}
.layer-controls {
display: flex;
gap: 4px;
align-items: center;
}
.layer-btn {
padding: 2px 4px;
font-size: 8px;
border: none;
background: transparent;
color: var(--muted);
}
.layer-btn:hover {
color: var(--text);
}
.layer-opacity {
width: 40px;
height: 3px;
}
.drop-zone {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 13px;
z-index: 5;
pointer-events: none;
}
.drop-zone.hidden {
display: none;
}
.dz-ring {
width: 60px;
height: 60px;
border: 2px dashed var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: var(--muted);
animation: pulse 2.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
border-color: var(--border);
transform: scale(1);
}
50% {
border-color: var(--accent);
transform: scale(1.04);
}
}
#toast {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--panel2);
border: 1px solid var(--border);
padding: 7px 18px;
border-radius: 20px;
font-size: 11px;
color: var(--text);
opacity: 0;
transition: all 0.3s;
pointer-events: none;
z-index: 100;
white-space: nowrap;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
#toast.on {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.zoom-bar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
display: none;
align-items: center;
gap: 6px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 20px;
padding: 3px 10px;
font-size: 11px;
color: var(--muted);
z-index: 10;
}
.zoom-bar.on {
display: flex;
}
.zoom-bar button {
background: transparent;
border: none;
color: var(--muted);
padding: 1px 5px;
font-size: 14px;
}
.zoom-bar button:hover {
color: var(--accent);
}
.modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 50;
display: none;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--panel);
padding: 20px;
border: 1px solid var(--border);
border-radius: var(--radius);
width: 300px;
}
.modal-content h3 {
margin: 0 0 10px 0;
font-family: var(--display);
font-size: 14px;
}
.modal-content input[type="text"] {
width: 100%;
background: var(--panel2);
border: 1px solid var(--border);
color: var(--text);
padding: 8px;
margin-bottom: 10px;
font-family: var(--mono);
border-radius: var(--radius);
}
.modal-content button {
width: 100%;
margin-bottom: 5px;
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
width: 0%;
transition: width 0.3s;
}
.color-presets {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-top: 4px;
}
.color-preset {
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
border: 1px solid var(--border);
}
.color-preset:hover {
border-color: var(--accent);
}
.brush-preview {
width: 100%;
height: 40px;
background: var(--panel2);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.brush-preview canvas {
border-radius: 50%;
}
.tool-options {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.tool-option-btn {
flex: 1;
padding: 6px;
font-size: 9px;
}
.tool-option-btn.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.shortcut-hint {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
font-size: 8px;
color: var(--muted);
font-family: var(--mono);
}
.status-text {
font-size: 9px;
color: var(--muted);
margin-top: 5px;
min-height: 12px;
}
.image-info {
display: flex;
gap: 10px;
font-size: 9px;
color: var(--muted);
margin-bottom: 10px;
padding: 6px;
background: var(--panel2);
border-radius: var(--radius);
}
.info-item {
display: flex;
flex-direction: column;
}
.info-item span:first-child {
color: var(--text);
font-weight: 500;
}
</style>
</head>
<body>
<!-- HEADER -->
<header>
<div class="logo">perchance <span>//</span> img.edit</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
<div class="header-actions">
<button onclick="undo()" title="Undo (Ctrl+Z)"><i class="ph ph-arrow-u-up-left"></i> Undo</button>
<button onclick="redo()" title="Redo (Ctrl+Y)"><i class="ph ph-arrow-u-up-right"></i> Redo</button>
<button class="btn-primary" onclick="exportImage()" title="Export"><i class="ph ph-download"></i> Export</button>
</div>
</header>
<!-- WORKSPACE -->
<div class="workspace">
<!-- LEFT PANEL -->
<div class="panel-left">
<div class="label-caps">Tools</div>
<div class="tool-grid">
<button class="tool-btn active" onclick="setTool('move')" title="Pan/Select">
<i class="ph ph-cursor"></i> Move
</button>
<button class="tool-btn" onclick="setTool('draw')" title="Brush">
<i class="ph ph-paint-brush"></i> Draw
</button>
<button class="tool-btn" onclick="setTool('erase')" title="Eraser">
<i class="ph ph-eraser"></i> Erase
</button>
<button class="tool-btn" onclick="setTool('text')" title="Add Text">
<i class="ph ph-text-t"></i> Text
</button>
<button class="tool-btn" onclick="setTool('rect')" title="Rectangle">
<i class="ph ph-rectangle"></i> Rect
</button>
<button class="tool-btn" onclick="setTool('circle')" title="Circle">
<i class="ph ph-circle"></i> Circle
</button>
<button class="tool-btn" onclick="setTool('line')" title="Line">
<i class="ph ph-line-segment"></i> Line
</button>
<button class="tool-btn" onclick="setTool('fill')" title="Fill">
<i class="ph ph-drop"></i> Fill
</button>
</div>
<div class="label-caps">Brush Settings</div>
<div class="brush-preview">
<canvas id="brushPreview" width="30" height="30"></canvas>
</div>
<div class="frow">
<div class="flabel"><span>Size</span><span class="fval" id="bSizeVal">10px</span></div>
<input type="range" id="brushSize" min="1" max="100" value="10" oninput="updateBrush()">
</div>
<div class="frow">
<div class="flabel"><span>Hardness</span><span class="fval" id="bHardVal">100%</span></div>
<input type="range" id="brushHardness" min="0" max="100" value="100" oninput="updateBrush()">
</div>
<div class="frow">
<div class="flabel"><span>Opacity</span><span class="fval" id="bOpVal">100%</span></div>
<input type="range" id="brushOpacity" min="1" max="100" value="100" oninput="updateBrush()">
</div>
<div class="frow">
<div class="flabel"><span>Color</span></div>
<input type="color" id="brushColor" value="#ffffff" oninput="updateBrush()">
</div>
<div class="color-presets">
<div class="color-preset" style="background:#fff" onclick="setColor('#ffffff')"></div>
<div class="color-preset" style="background:#000" onclick="setColor('#000000')"></div>
<div class="color-preset" style="background:#f76f6f" onclick="setColor('#f76f6f')"></div>
<div class="color-preset" style="background:#f7a26f" onclick="setColor('#f7a26f')"></div>
<div class="color-preset" style="background:#5de8a0" onclick="setColor('#5de8a0')"></div>
<div class="color-preset" style="background:#7c6ff7" onclick="setColor('#7c6ff7')"></div>
</div>
<div class="label-caps">Text Settings</div>
<div class="frow">
<div class="flabel"><span>Font Size</span><span class="fval" id="fontSizeVal">24px</span></div>
<input type="range" id="fontSize" min="10" max="200" value="24" oninput="updateTextSettings()">
</div>
<div class="frow">
<div class="flabel"><span>Font</span></div>
<select id="fontFamily" onchange="updateTextSettings()">
<option value="DM Mono">DM Mono</option>
<option value="Syne">Syne</option>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="frow">
<div class="flabel"><span>Style</span></div>
<select id="fontStyle" onchange="updateTextSettings()">
<option value="normal">Normal</option>
<option value="bold">Bold</option>
<option value="italic">Italic</option>
</select>
</div>
</div>
<!-- CANVAS -->
<div class="canvas-area" id="canvasArea" oncontextmenu="return false;">
<div class="drop-zone" id="dropZone">
<div class="dz-ring"><i class="ph ph-image"></i></div>
<div style="font-family:var(--display);font-weight:700;">Drop Image</div>
<button class="btn-primary" onclick="document.getElementById('fileInput').click()">Browse Files</button>
</div>
<div class="canvas-centerer">
<div class="canvas-wrap" id="canvasWrap">
<canvas id="mainCanvas"></canvas>
</div>
</div>
<div class="zoom-bar" id="zoomBar">
<button onclick="zoomOut()">-</button>
<span id="zoomLabel">100%</span>
<button onclick="zoomIn()">+</button>
<button onclick="fitCanvas()">Fit</button>
<button onclick="zoom1()">1:1</button>
</div>
</div>
<!-- RIGHT PANEL -->
<div class="panel-right">
<div class="tab-row">
<button class="tab active" onclick="switchTab('layers')">Layers</button>
<button class="tab" onclick="switchTab('adjust')">Adjust</button>
<button class="tab" onclick="switchTab('upscale')">Upscale</button>
<button class="tab" onclick="switchTab('bg')">Remove BG</button>
<button class="tab" onclick="switchTab('crop')">Crop</button>
</div>
<!-- Layers Tab -->
<div class="tab-panel active" id="tab-layers">
<div class="image-info" id="imageInfo" style="display:none;">
<div class="info-item">
<span>Width</span>
<span id="imgWidth">0px</span>
</div>
<div class="info-item">
<span>Height</span>
<span id="imgHeight">0px</span>
</div>
</div>
<div class="label-caps">Layer Stack</div>
<div style="margin-bottom:10px;display:flex;gap:6px;">
<button style="flex:1" onclick="addNewLayer()">+ New Layer</button>
<button onclick="duplicateLayer()"><i class="ph ph-copy"></i></button>
</div>
<div class="layer-list" id="layerList"></div>
</div>
<!-- Adjust Tab -->
<div class="tab-panel" id="tab-adjust">
<div class="label-caps">Filters</div>
<div class="frow">
<div class="flabel"><span>Brightness</span><span class="fval" id="val-brightness">0</span></div>
<input type="range" id="brightness" min="-100" max="100" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Contrast</span><span class="fval" id="val-contrast">0</span></div>
<input type="range" id="contrast" min="-100" max="100" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Saturation</span><span class="fval" id="val-saturation">0</span></div>
<input type="range" id="saturation" min="-100" max="100" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Blur</span><span class="fval" id="val-blur">0px</span></div>
<input type="range" id="blur" min="0" max="20" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Hue Rotate</span><span class="fval" id="val-hue">0deg</span></div>
<input type="range" id="hue" min="-180" max="180" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Invert</span><span class="fval" id="val-invert">0%</span></div>
<input type="range" id="invert" min="0" max="100" value="0" oninput="applyFilters()">
</div>
<div class="frow">
<div class="flabel"><span>Presets</span></div>
<select onchange="applyPreset(this.value)">
<option value="none">None</option>
<option value="warm">Warm Tone</option>
<option value="cool">Cool Tone</option>
<option value="bw">Black & White</option>
<option value="vintage">Vintage</option>
<option value="sepia">Sepia</option>
<option value="vibrant">Vibrant</option>
<option value="dramatic">Dramatic</option>
</select>
</div>
<button class="btn" style="width:100%" onclick="resetFilters()">Reset All</button>
</div>
<!-- Upscale Tab -->
<div class="tab-panel" id="tab-upscale">
<div class="label-caps">AI Upscale Pipeline</div>
<div class="ai-card">
<p>Select a model from the pipeline. Models are cached per session.</p>
<div class="model-info" id="upscaleInfoText">Select a model below...</div>
</div>
<div class="frow">
<div class="flabel"><span>Model Selection</span></div>
<select id="upscaleModel" onchange="updateUpscaleInfo()">
<option value="bicubic">Bicubic (Fast, No AI)</option>
<option value="realesrgan_x4plus">Real-ESRGAN ×4 Plus</option>
<option value="realesrgan_general_x4">Real-ESR General ×4</option>
<option value="realesrgan_x2plus">Real-ESRGAN ×2 Plus</option>
<option value="bsrgan_x2">BSRGAN ×2</option>
<option value="swinir_bsrgan_x4">SwinIR BSRGAN ×4</option>
<option value="swin2sr_classical_x4">Swin2SR Classical ×4</option>
<option value="swin2sr_classical_x2">Swin2SR Classical ×2</option>
<option value="swinir_noise">SwinIR Noise Reduction</option>
<option value="ultrasharp_x4">UltraSharp ×4</option>
<option value="ultramix_smooth_x4">UltraMix Smooth ×4</option>
</select>
</div>
<div class="frow">
<div class="flabel"><span>Scale Factor</span><span class="fval" id="scaleVal">2x</span></div>
<input type="range" id="upscaleScale" min="2" max="4" step="1" value="2" oninput="getEl('scaleVal').textContent=this.value+'x'">
</div>
<button class="btn-primary" style="width:100%" onclick="runUpscale()">Run Upscale</button>
<div id="upscaleStatus" class="status-text"></div>
<div class="progress-bar" id="upscaleProgress" style="display:none;">
<div class="progress-fill" id="upscaleProgressFill"></div>
</div>
</div>
<!-- Remove BG Tab -->
<div class="tab-panel" id="tab-bg">
<div class="label-caps">Background Removal</div>
<div class="ai-card">
<p>Uses @imgly/background-removal. Runs locally in browser.</p>
</div>
<button class="btn-primary" style="width:100%" onclick="removeBackground()">Remove Background</button>
<div id="bgStatus" class="status-text"></div>
<div class="progress-bar" id="bgProgress" style="display:none;">
<div class="progress-fill" id="bgProgressFill"></div>
</div>
</div>
<!-- Crop Tab -->
<div class="tab-panel" id="tab-crop">
<div class="label-caps">Crop & Transform</div>
<div class="frow">
<div class="flabel"><span>Width</span><span class="fval" id="cropW">0</span></div>
<input type="range" id="cropWidth" min="1" max="100" value="100" oninput="updateCropPreview()">
</div>
<div class="frow">
<div class="flabel"><span>Height</span><span class="fval" id="cropH">0</span></div>
<input type="range" id="cropHeight" min="1" max="100" value="100" oninput="updateCropPreview()">
</div>
<div class="tool-options">
<button class="tool-option-btn active" onclick="setCropAnchor('tl')">TL</button>
<button class="tool-option-btn" onclick="setCropAnchor('tc')">TC</button>
<button class="tool-option-btn" onclick="setCropAnchor('tr')">TR</button>
<button class="tool-option-btn" onclick="setCropAnchor('ml')">ML</button>
<button class="tool-option-btn" onclick="setCropAnchor('mc')">MC</button>
<button class="tool-option-btn" onclick="setCropAnchor('mr')">MR</button>
<button class="tool-option-btn" onclick="setCropAnchor('bl')">BL</button>
<button class="tool-option-btn" onclick="setCropAnchor('bc')">BC</button>
<button class="tool-option-btn" onclick="setCropAnchor('br')">BR</button>
</div>
<button class="btn-primary" style="width:100%" onclick="applyCrop()">Apply Crop</button>
<button style="width:100%;margin-top:8px;" onclick="rotateCanvas(-90)">Rotate -90°</button>
<button style="width:100%;margin-top:4px;" onclick="rotateCanvas(90)">Rotate +90°</button>
<button style="width:100%;margin-top:4px;" onclick="flipHorizontal()">Flip Horizontal</button>
<button style="width:100%;margin-top:4px;" onclick="flipVertical()">Flip Vertical</button>
</div>
</div>
</div>
<!-- MODALS -->
<div class="modal" id="textModal">
<div class="modal-content">
<h3>Add Text</h3>
<input type="text" id="textInput" placeholder="Enter text...">
<button class="btn-primary" onclick="addTextToCanvas()">Add Text</button>
<button style="margin-top:5px;" onclick="closeModal('textModal'); setTool('move')">Cancel</button>
</div>
</div>
<input type="file" id="fileInput" accept="image/*" style="display:none" onchange="loadImage(this.files[0])">
<div id="toast">Message</div>
<script>
// --- UTILS ---
const getEl = id => document.getElementById(id);
const toast = (msg, duration = 3000) => {
const t = getEl('toast');
t.textContent = msg;
t.classList.add('on');
setTimeout(() => t.classList.remove('on'), duration);
};
// --- STATE ---
let layers = [];
let activeLayerId = null;
let zoomLevel = 1;
let panStart = null;
let panScroll = null;
let isPanning = false;
let currentTool = 'move';
let isDrawing = false;
let drawingLayer = null;
let shapeStart = null;
let lastPoint = null;
let cropAnchor = 'tl';
// --- HISTORY ---
const history = {
stack: [],
pointer: -1,
maxSize: 50,
push: function(state) {
if (this.pointer < this.stack.length - 1) {
this.stack = this.stack.slice(0, this.pointer + 1);
}
this.stack.push(JSON.stringify(state));
if (this.stack.length > this.maxSize) {
this.stack.shift();
}
this.pointer = this.stack.length - 1;
},
undo: function() {
if (this.pointer > 0) {
this.pointer--;
restoreState(JSON.parse(this.stack[this.pointer]));
toast('Undo');
}
},
redo: function() {
if (this.pointer < this.stack.length - 1) {
this.pointer++;
restoreState(JSON.parse(this.stack[this.pointer]));
toast('Redo');
}
}
};
function saveState() {
const state = layers.map(l => ({
id: l.id,
type: l.type,
name: l.name,
data: l.canvas.toDataURL('image/png'),
x: l.x,
y: l.y,
visible: l.visible,
opacity: l.opacity
}));
history.push(state);
}
function restoreState(state) {
const loadPromises = state.map(l => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve({
id: l.id,
type: l.type,
name: l.name,
canvas: canvas,
x: l.x,
y: l.y,
visible: l.visible,
opacity: l.opacity
});
};
img.src = l.data;
});
});
Promise.all(loadPromises).then(loadedLayers => {
layers = loadedLayers;
activeLayerId = layers[0]?.id || null;
renderLayersList();
renderLayers();
updateImageInfo();
});
}
// --- INIT ---
function init() {
getEl('canvasArea').addEventListener('contextmenu', e => e.preventDefault());
// Pan & Draw handlers
getEl('canvasArea').addEventListener('mousedown', handleMouseDown);
getEl('canvasArea').addEventListener('mousemove', handleMouseMove);
getEl('canvasArea').addEventListener('mouseup', handleMouseUp);
getEl('canvasArea').addEventListener('mouseleave', handleMouseUp);
// Wheel zoom
getEl('canvasArea').addEventListener('wheel', handleWheel);
// Drop zone
window.addEventListener('dragover', e => e.preventDefault());
window.addEventListener('drop', e => {
e.preventDefault();
if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]);
});
// Keyboard
window.addEventListener('keydown', handleKeyDown);
updateBrush();
updateUpscaleInfo();
updateTextSettings();
}
function handleMouseDown(e) {
if (e.button === 1 || (currentTool === 'move' && e.button === 0)) {
e.preventDefault();
isPanning = true;
panStart = { x: e.clientX, y: e.clientY };
panScroll = { x: getEl('canvasArea').scrollLeft, y: getEl('canvasArea').scrollTop };
getEl('canvasArea').style.cursor = 'grabbing';
} else if (e.button === 0 && activeLayerId) {
if (currentTool === 'draw' || currentTool === 'erase') {
startDrawing(e);
} else if (currentTool === 'rect' || currentTool === 'circle' || currentTool === 'line') {
startShape(e);
} else if (currentTool === 'fill') {
floodFill(e);
} else if (currentTool === 'text') {
showTextModal(e);
}
}
}
function handleMouseMove(e) {
if (isPanning && panStart && panScroll) {
getEl('canvasArea').scrollLeft = panScroll.x - (e.clientX - panStart.x);
getEl('canvasArea').scrollTop = panScroll.y - (e.clientY - panStart.y);
}
if (isDrawing && drawingLayer) {
const pt = getMousePos(drawingLayer.canvas, e);
const ctx = drawingLayer.canvas.getContext('2d');
if (currentTool === 'draw') {
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = getEl('brushColor').value;
ctx.lineWidth = getEl('brushSize').value;
ctx.globalAlpha = getEl('brushOpacity').value / 100;
if (lastPoint) {
ctx.beginPath();
ctx.moveTo(lastPoint.x, lastPoint.y);
ctx.lineTo(pt.x, pt.y);
ctx.stroke();
}
lastPoint = pt;
} else if (currentTool === 'erase') {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = getEl('brushSize').value;
if (lastPoint) {
ctx.beginPath();
ctx.moveTo(lastPoint.x, lastPoint.y);
ctx.lineTo(pt.x, pt.y);
ctx.stroke();
}
lastPoint = pt;
ctx.globalCompositeOperation = 'source-over';
}
renderLayers();
}
}
function handleMouseUp() {
if (isPanning) {
isPanning = false;
panStart = null;
panScroll = null;
get