00face commited on
Commit
1dee760
·
verified ·
1 Parent(s): 8e8fcb3

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +431 -290
index.html CHANGED
@@ -147,6 +147,17 @@
147
  .btn-primary:hover {
148
  background: #9187fa;
149
  border-color: #9187fa;
 
 
 
 
 
 
 
 
 
 
 
150
  }
151
 
152
  .workspace {
@@ -198,6 +209,7 @@
198
  font-size: 9px;
199
  cursor: pointer;
200
  height: 55px;
 
201
  }
202
 
203
  .tool-btn:hover {
@@ -212,7 +224,7 @@
212
  color: var(--accent);
213
  }
214
 
215
- .tool-btn svg {
216
  width: 18px;
217
  height: 18px;
218
  }
@@ -246,7 +258,6 @@
246
  border-radius: 2px;
247
  transition: transform 0.1s;
248
  background: transparent;
249
- /* Transform origin center for zoom */
250
  transform-origin: center center;
251
  }
252
 
@@ -290,7 +301,7 @@
290
 
291
  .tab:hover {
292
  color: var(--text);
293
- background: rgba(255,255,255,0.02);
294
  }
295
 
296
  .tab.active {
@@ -331,7 +342,7 @@
331
  text-align: right;
332
  }
333
 
334
- input[type=range] {
335
  -webkit-appearance: none;
336
  width: 100%;
337
  height: 3px;
@@ -341,7 +352,7 @@
341
  cursor: pointer;
342
  }
343
 
344
- input[type=range]::-webkit-slider-thumb {
345
  -webkit-appearance: none;
346
  width: 12px;
347
  height: 12px;
@@ -351,10 +362,18 @@
351
  transition: transform 0.1s;
352
  }
353
 
354
- input[type=range]:hover::-webkit-slider-thumb {
355
  transform: scale(1.2);
356
  }
357
 
 
 
 
 
 
 
 
 
358
  select {
359
  width: 100%;
360
  background: var(--panel2);
@@ -381,6 +400,7 @@
381
  font-size: 10px;
382
  color: var(--muted);
383
  line-height: 1.5;
 
384
  }
385
 
386
  .model-info {
@@ -390,7 +410,6 @@
390
  font-style: italic;
391
  }
392
 
393
- /* Layer Styling */
394
  .layer-list {
395
  display: flex;
396
  flex-direction: column;
@@ -406,6 +425,7 @@
406
  border-radius: 4px;
407
  border: 1px solid var(--border);
408
  position: relative;
 
409
  }
410
 
411
  .layer-item.active {
@@ -419,6 +439,7 @@
419
  background: #000;
420
  object-fit: cover;
421
  border: 1px solid var(--border);
 
422
  }
423
 
424
  .layer-info {
@@ -451,6 +472,11 @@
451
  color: var(--text);
452
  }
453
 
 
 
 
 
 
454
  .drop-zone {
455
  position: absolute;
456
  inset: 0;
@@ -507,7 +533,7 @@
507
  pointer-events: none;
508
  z-index: 100;
509
  white-space: nowrap;
510
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
511
  }
512
 
513
  #toast.on {
@@ -548,16 +574,19 @@
548
  color: var(--accent);
549
  }
550
 
551
- /* Modal for Text */
552
  .modal {
553
  position: absolute;
554
- top: 0; left: 0; width: 100%; height: 100%;
555
- background: rgba(0,0,0,0.7);
 
 
 
556
  z-index: 50;
557
  display: none;
558
  align-items: center;
559
  justify-content: center;
560
  }
 
561
  .modal-content {
562
  background: var(--panel);
563
  padding: 20px;
@@ -565,6 +594,135 @@
565
  border-radius: var(--radius);
566
  width: 300px;
567
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  </style>
569
  </head>
570
 
@@ -575,9 +733,9 @@
575
  <div class="logo">perchance <span>//</span> img.edit</div>
576
  <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
577
  <div class="header-actions">
578
- <button onclick="history.undo()" title="Undo (Ctrl+Z)"><phosphor-icon name="arrow-u-left"></phosphor-icon> Undo</button>
579
- <button onclick="history.redo()" title="Redo (Ctrl+Y)"><phosphor-icon name="arrow-u-right"></phosphor-icon> Redo</button>
580
- <button class="btn-primary" onclick="exportImage()" title="Export"><phosphor-icon name="download"></phosphor-icon> Export</button>
581
  </div>
582
  </header>
583
 
@@ -589,29 +747,42 @@
589
  <div class="label-caps">Tools</div>
590
  <div class="tool-grid">
591
  <button class="tool-btn active" onclick="setTool('move')" title="Pan/Select">
592
- <phosphor-icon name="cursor"></phosphor-icon> Move
593
  </button>
594
  <button class="tool-btn" onclick="setTool('draw')" title="Brush">
595
- <phosphor-icon name="brush"></phosphor-icon> Draw
596
  </button>
597
  <button class="tool-btn" onclick="setTool('erase')" title="Eraser">
598
- <phosphor-icon name="eraser"></phosphor-icon> Erase
599
  </button>
600
  <button class="tool-btn" onclick="setTool('text')" title="Add Text">
601
- <phosphor-icon name="text-t"></phosphor-icon> Text
602
  </button>
603
  <button class="tool-btn" onclick="setTool('rect')" title="Rectangle">
604
- <phosphor-icon name="rectangle"></phosphor-icon> Rect
605
  </button>
606
- <button class="tool-btn" onclick="flipH()" title="Flip Horizontal">
607
- <phosphor-icon name="arrows-horizontal"></phosphor-icon> Flip
 
 
 
 
 
 
608
  </button>
609
  </div>
610
 
611
- <div class="label-caps">Properties</div>
 
 
 
612
  <div class="frow">
613
- <div class="flabel"><span>Size</span><span class="fval" id="bSizeVal">5px</span></div>
614
- <input type="range" id="brushSize" min="1" max="100" value="5" oninput="updateBrush()">
 
 
 
 
615
  </div>
616
  <div class="frow">
617
  <div class="flabel"><span>Opacity</span><span class="fval" id="bOpVal">100%</span></div>
@@ -619,56 +790,93 @@
619
  </div>
620
  <div class="frow">
621
  <div class="flabel"><span>Color</span></div>
622
- <input type="color" id="brushColor" value="#ffffff" style="width:100%;height:30px;border:none;background:transparent;">
 
 
 
 
 
 
 
 
623
  </div>
 
 
624
  <div class="frow">
625
  <div class="flabel"><span>Font Size</span><span class="fval" id="fontSizeVal">24px</span></div>
626
- <input type="range" id="fontSize" min="10" max="200" value="24" oninput="updateBrush()">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  </div>
628
-
629
  </div>
630
 
631
  <!-- CANVAS -->
632
  <div class="canvas-area" id="canvasArea" oncontextmenu="return false;">
633
  <div class="drop-zone" id="dropZone">
634
- <div class="dz-ring"><phosphor-icon name="image"></phosphor-icon></div>
635
  <div style="font-family:var(--display);font-weight:700;">Drop Image</div>
636
  <button class="btn-primary" onclick="document.getElementById('fileInput').click()">Browse Files</button>
637
  </div>
638
 
639
  <div class="canvas-centerer">
640
  <div class="canvas-wrap" id="canvasWrap">
641
- <!-- Main Composite Canvas -->
642
  <canvas id="mainCanvas"></canvas>
643
  </div>
644
  </div>
645
 
646
  <div class="zoom-bar" id="zoomBar">
647
- <button onclick="zoom(-0.1)">-</button>
648
  <span id="zoomLabel">100%</span>
649
- <button onclick="zoom(0.1)">+</button>
650
  <button onclick="fitCanvas()">Fit</button>
 
651
  </div>
652
  </div>
653
 
654
  <!-- RIGHT PANEL -->
655
  <div class="panel-right">
656
  <div class="tab-row">
657
- <button class="tab active" onclick="switchTab('layers')" data-tooltip="Manage visibility & order">Layers</button>
658
- <button class="tab" onclick="switchTab('adjust')" data-tooltip="Basic color correction">Adjust</button>
659
- <button class="tab" onclick="switchTab('upscale')" data-tooltip="AI & Algorithmic Upscaling">Upscale</button>
660
- <button class="tab" onclick="switchTab('bg')" data-tooltip="Remove backgrounds">Remove BG</button>
 
661
  </div>
662
 
663
  <!-- Layers Tab -->
664
  <div class="tab-panel active" id="tab-layers">
665
- <div class="label-caps">Layer Stack</div>
666
- <div style="margin-bottom:10px;">
667
- <button class="btn-primary" style="width:100%" onclick="addNewLayer()">+ New Layer</button>
 
 
 
 
 
 
668
  </div>
669
- <div class="layer-list" id="layerList">
670
- <!-- Layers injected here -->
 
 
671
  </div>
 
672
  </div>
673
 
674
  <!-- Adjust Tab -->
@@ -694,6 +902,10 @@
694
  <div class="flabel"><span>Hue Rotate</span><span class="fval" id="val-hue">0deg</span></div>
695
  <input type="range" id="hue" min="-180" max="180" value="0" oninput="applyFilters()">
696
  </div>
 
 
 
 
697
  <div class="frow">
698
  <div class="flabel"><span>Presets</span></div>
699
  <select onchange="applyPreset(this.value)">
@@ -702,6 +914,9 @@
702
  <option value="cool">Cool Tone</option>
703
  <option value="bw">Black & White</option>
704
  <option value="vintage">Vintage</option>
 
 
 
705
  </select>
706
  </div>
707
  <button class="btn" style="width:100%" onclick="resetFilters()">Reset All</button>
@@ -739,17 +954,52 @@
739
  </div>
740
 
741
  <button class="btn-primary" style="width:100%" onclick="runUpscale()">Run Upscale</button>
742
- <div id="upscaleStatus" style="font-size:9px;color:var(--muted);margin-top:5px;min-height:14px;"></div>
 
 
 
743
  </div>
744
 
745
  <!-- Remove BG Tab -->
746
  <div class="tab-panel" id="tab-bg">
747
  <div class="label-caps">Background Removal</div>
748
  <div class="ai-card">
749
- <p>Uses @imgly/background-removal. Runs locally.</p>
750
  </div>
751
  <button class="btn-primary" style="width:100%" onclick="removeBackground()">Remove Background</button>
752
- <div id="bgStatus" style="font-size:9px;color:var(--muted);margin-top:5px;"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  </div>
754
  </div>
755
  </div>
@@ -757,10 +1007,10 @@
757
  <!-- MODALS -->
758
  <div class="modal" id="textModal">
759
  <div class="modal-content">
760
- <h3 style="margin:0 0 10px 0; font-family:var(--display)">Add Text</h3>
761
- <input type="text" id="textInput" placeholder="Enter text..." style="width:100%; background:var(--panel2); border:1px solid var(--border); color:var(--text); padding:8px; margin-bottom:10px; font-family:var(--mono)">
762
- <button class="btn-primary" style="width:100%" onclick="addTextToCanvas()">Add Text</button>
763
- <button style="width:100%; margin-top:5px;" onclick="document.getElementById('textModal').style.display='none'; setTool('move')">Cancel</button>
764
  </div>
765
  </div>
766
 
@@ -770,309 +1020,200 @@
770
  <script>
771
  // --- UTILS ---
772
  const getEl = id => document.getElementById(id);
773
- const toast = msg => {
774
  const t = getEl('toast');
775
  t.textContent = msg;
776
  t.classList.add('on');
777
- setTimeout(() => t.classList.remove('on'), 3000);
778
  };
779
 
780
- // --- HISTORY SYSTEM ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  const history = {
782
  stack: [],
783
  pointer: -1,
 
784
  push: function(state) {
785
- this.stack.push(state);
786
- this.pointer++;
 
 
 
 
 
 
787
  },
788
  undo: function() {
789
  if (this.pointer > 0) {
790
  this.pointer--;
791
- restoreState(this.stack[this.pointer]);
792
  toast('Undo');
793
  }
794
  },
795
  redo: function() {
796
  if (this.pointer < this.stack.length - 1) {
797
  this.pointer++;
798
- restoreState(this.stack[this.pointer]);
799
  toast('Redo');
800
  }
801
  }
802
  };
803
 
804
  function saveState() {
805
- // Save current layer stack data
806
  const state = layers.map(l => ({
807
  id: l.id,
808
  type: l.type,
809
- data: l.canvas.toDataURL(),
810
- x: l.x, y: l.y,
 
 
811
  visible: l.visible,
812
- opacity: l.opacity,
813
- text: l.text
814
  }));
815
  history.push(state);
816
  }
817
 
818
  function restoreState(state) {
819
- layers = state.map(l => {
820
- const img = new Image();
821
- img.src = l.data;
822
- const canvas = document.createElement('canvas');
823
- canvas.width = img.width;
824
- canvas.height = img.height;
825
- canvas.getContext('2d').drawImage(img, 0, 0);
826
-
827
- return {
828
- id: l.id,
829
- type: l.type,
830
- canvas: canvas,
831
- x: l.x, y: l.y,
832
- visible: l.visible,
833
- opacity: l.opacity,
834
- text: l.text
835
- };
 
 
 
 
 
836
  });
837
- renderLayers();
838
- renderLayersList();
839
- fitCanvas();
840
- }
841
 
842
- // --- STATE ---
843
- let layers = []; // Array of layer objects
844
- let activeLayerId = null;
845
- let zoom = 1;
846
- let panStart = null;
847
- let panScroll = null;
848
- let isPanning = false;
849
- let currentTool = 'move';
850
- let isDrawing = false;
851
- let drawingLayer = null; // The layer currently being drawn on
852
- let shapeStart = null; // For rectangle tool
853
 
854
  // --- INIT ---
855
  function init() {
856
- const wrap = getEl('canvasWrap');
857
  getEl('canvasArea').addEventListener('contextmenu', e => e.preventDefault());
858
 
859
- // Pan Logic (Mouse)
860
- getEl('canvasArea').addEventListener('mousedown', e => {
861
- if (e.button === 1 || (currentTool === 'move' && e.button === 0)) {
862
- e.preventDefault();
863
- isPanning = true;
864
- panStart = { x: e.clientX, y: e.clientY };
865
- panScroll = { x: getEl('canvasArea').scrollLeft, y: getEl('canvasArea').scrollTop };
866
- getEl('canvasArea').style.cursor = 'grabbing';
867
- } else if (currentTool === 'draw' && activeLayerId) {
868
- startDrawing(e);
869
- } else if (currentTool === 'rect' && activeLayerId) {
870
- startShape(e);
871
- }
872
- });
873
-
874
- getEl('canvasArea').addEventListener('mousemove', e => {
875
- if (isPanning && panStart && panScroll) {
876
- getEl('canvasArea').scrollLeft = panScroll.x - (e.clientX - panStart.x);
877
- getEl('canvasArea').scrollTop = panScroll.y - (e.clientY - panStart.y);
878
- }
879
 
880
- if (currentTool === 'draw' && isDrawing && drawingLayer) {
881
- const pt = getMousePos(drawingLayer.canvas, e);
882
- const ctx = drawingLayer.canvas.getContext('2d');
883
- ctx.lineTo(pt.x, pt.y);
884
- ctx.stroke();
885
- }
886
 
887
- if (currentTool === 'rect' && isDrawing && drawingLayer) {
888
- // Update preview rect
889
- const ctx = drawingLayer.canvas.getContext('2d');
890
- // Clear and redraw logic would be needed for a true preview,
891
- // but for simplicity we just draw directly here (destructive in this simple impl)
892
- // A better way is to have a temp canvas, but we'll stick to direct drawing for this snippet size
893
- }
894
- });
895
-
896
- getEl('canvasArea').addEventListener('mouseup', () => {
897
- if (isPanning) {
898
- isPanning = false;
899
- panStart = null;
900
- panScroll = null;
901
- getEl('canvasArea').style.cursor = 'default';
902
- }
903
- if (isDrawing) {
904
- isDrawing = false;
905
- drawingLayer.canvas.getContext('2d').closePath();
906
- saveState();
907
- drawingLayer = null;
908
- }
909
- if (currentTool === 'rect' && shapeStart) {
910
- // finalize shape
911
- shapeStart = null;
912
- saveState();
913
- drawingLayer = null;
914
- }
915
- });
916
-
917
- // Drop Zone
918
  window.addEventListener('dragover', e => e.preventDefault());
919
  window.addEventListener('drop', e => {
920
  e.preventDefault();
921
  if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]);
922
  });
923
 
924
- // Keyboard shortcuts
925
- window.addEventListener('keydown', e => {
926
- if ((e.ctrlKey || e.metaKey) && e.key === 'z') history.undo();
927
- if ((e.ctrlKey || e.metaKey) && e.key === 'y') history.redo();
928
- });
929
 
930
  updateBrush();
931
  updateUpscaleInfo();
 
932
  }
933
 
934
- // --- LAYER SYSTEM ---
935
- function addNewLayer() {
936
- if (!layers.length) {
937
- toast("Please load an image first");
938
- return;
 
 
 
 
 
 
 
 
 
 
 
 
939
  }
940
- const baseL = layers[0]; // Reference size
941
- const newCanvas = document.createElement('canvas');
942
- newCanvas.width = baseL.canvas.width;
943
- newCanvas.height = baseL.canvas.height;
944
-
945
- const newLayer = {
946
- id: Date.now(),
947
- type: 'draw',
948
- canvas: newCanvas,
949
- x: 0, y: 0,
950
- visible: true,
951
- opacity: 1,
952
- name: `Layer ${layers.length + 1}`
953
- };
954
-
955
- layers.push(newLayer);
956
- activeLayerId = newLayer.id;
957
- renderLayersList();
958
- renderLayers();
959
- saveState();
960
- toast('New Layer Added');
961
- }
962
-
963
- function renderLayersList() {
964
- const list = getEl('layerList');
965
- list.innerHTML = '';
966
-
967
- // Reverse to show top layer at top
968
- [...layers].reverse().forEach((l, index) => {
969
- const realIndex = layers.length - 1 - index;
970
- const div = document.createElement('div');
971
- div.className = `layer-item ${l.id === activeLayerId ? 'active' : ''}`;
972
- div.onclick = () => { activeLayerId = l.id; renderLayersList(); renderLayers(); };
973
-
974
- // Thumbnail
975
- const thumb = document.createElement('canvas');
976
- thumb.className = 'layer-thumb';
977
- thumb.width = 32; thumb.height = 32;
978
- const tCtx = thumb.getContext('2d');
979
- tCtx.drawImage(l.canvas, 0, 0, 32, 32);
980
-
981
- div.innerHTML = `
982
- <phosphor-icon name="eye" style="color:${l.visible ? 'var(--text)' : 'var(--muted)}" onclick="toggleLayerVisibility(${l.id})"></phosphor-icon>
983
- <canvas width="32" height="32" class="layer-thumb"></canvas>
984
- <div class="layer-info">
985
- <div class="layer-name">${l.name || l.type}</div>
986
- <div class="layer-controls">
987
- <input type="range" min="0" max="1" step="0.1" value="${l.opacity}" style="width:40px" onchange="updateLayerOpacity(${l.id}, this.value)">
988
- <button class="layer-btn" onclick="deleteLayer(${l.id})"><phosphor-icon name="trash"></phosphor-icon></button>
989
- </div>
990
- </div>
991
- `;
992
-
993
- // Copy thumb content
994
- const thumbCanvas = div.querySelector('canvas');
995
- thumbCanvas.getContext('2d').drawImage(l.canvas, 0, 0, 32, 32);
996
-
997
- list.appendChild(div);
998
- });
999
  }
1000
 
1001
- function toggleLayerVisibility(id) {
1002
- const l = layers.find(x => x.id === id);
1003
- if(l) {
1004
- l.visible = !l.visible;
1005
- renderLayersList();
1006
- renderLayers();
1007
  }
1008
- }
1009
 
1010
- function deleteLayer(id) {
1011
- layers = layers.filter(l => l.id !== id);
1012
- if(layers.length === 0) activeLayerId = null;
1013
- else activeLayerId = layers[0].id;
1014
- renderLayersList();
1015
- renderLayers();
1016
- saveState();
1017
- }
1018
-
1019
- function updateLayerOpacity(id, val) {
1020
- const l = layers.find(x => x.id === id);
1021
- if(l) {
1022
- l.opacity = parseFloat(val);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  renderLayers();
1024
  }
1025
  }
1026
 
1027
- function renderLayers() {
1028
- const main = getEl('mainCanvas');
1029
- if(layers.length === 0) return;
1030
-
1031
- // Set main canvas size to match base layer
1032
- const base = layers[0];
1033
- main.width = base.canvas.width;
1034
- main.height = base.canvas.height;
1035
- const ctx = main.getContext('2d');
1036
- ctx.clearRect(0, 0, main.width, main.height);
1037
-
1038
- // Apply global filters to context
1039
- const b = getEl('brightness').value;
1040
- const c = getEl('contrast').value;
1041
- const s = getEl('saturation').value;
1042
- const bl = getEl('blur').value;
1043
- const h = getEl('hue').value;
1044
- ctx.filter = `brightness(${1 + b/100}) contrast(${1 + c/100}) saturate(${1 + s/100}) blur(${bl}px) hue(${h}deg)`;
1045
-
1046
- // Composite layers
1047
- layers.forEach(l => {
1048
- if(l.visible) {
1049
- ctx.globalAlpha = l.opacity;
1050
- ctx.drawImage(l.canvas, l.x, l.y);
1051
- }
1052
- });
1053
- ctx.globalAlpha = 1;
1054
- }
1055
-
1056
- // --- CORE IMAGE LOGIC ---
1057
- function loadImage(file) {
1058
- const reader = new FileReader();
1059
- reader.onload = e => {
1060
- const img = new Image();
1061
- img.onload = () => {
1062
- getEl('dropZone').classList.add('hidden');
1063
- getEl('zoomBar').classList.add('on');
1064
-
1065
- // Create Base Layer
1066
- const baseCanvas = document.createElement('canvas');
1067
- baseCanvas.width = img.width;
1068
- baseCanvas.height = img.height;
1069
- const ctx = baseCanvas.getContext('2d');
1070
- ctx.drawImage(img, 0, 0);
1071
-
1072
- layers = [{
1073
- id: 'base',
1074
- type: 'base',
1075
- canvas: baseCanvas,
1076
- x: 0, y: 0,
1077
- visible: true,
1078
- opacity: 1,
 
147
  .btn-primary:hover {
148
  background: #9187fa;
149
  border-color: #9187fa;
150
+ color: #fff;
151
+ }
152
+
153
+ .btn-danger {
154
+ background: var(--red);
155
+ color: #fff;
156
+ border: 1px solid transparent;
157
+ }
158
+
159
+ .btn-danger:hover {
160
+ background: #e85555;
161
  }
162
 
163
  .workspace {
 
209
  font-size: 9px;
210
  cursor: pointer;
211
  height: 55px;
212
+ font-family: var(--mono);
213
  }
214
 
215
  .tool-btn:hover {
 
224
  color: var(--accent);
225
  }
226
 
227
+ .tool-btn svg, .tool-btn i {
228
  width: 18px;
229
  height: 18px;
230
  }
 
258
  border-radius: 2px;
259
  transition: transform 0.1s;
260
  background: transparent;
 
261
  transform-origin: center center;
262
  }
263
 
 
301
 
302
  .tab:hover {
303
  color: var(--text);
304
+ background: rgba(255, 255, 255, 0.02);
305
  }
306
 
307
  .tab.active {
 
342
  text-align: right;
343
  }
344
 
345
+ input[type="range"] {
346
  -webkit-appearance: none;
347
  width: 100%;
348
  height: 3px;
 
352
  cursor: pointer;
353
  }
354
 
355
+ input[type="range"]::-webkit-slider-thumb {
356
  -webkit-appearance: none;
357
  width: 12px;
358
  height: 12px;
 
362
  transition: transform 0.1s;
363
  }
364
 
365
+ input[type="range"]:hover::-webkit-slider-thumb {
366
  transform: scale(1.2);
367
  }
368
 
369
+ input[type="color"] {
370
+ width: 100%;
371
+ height: 30px;
372
+ border: none;
373
+ background: transparent;
374
+ cursor: pointer;
375
+ }
376
+
377
  select {
378
  width: 100%;
379
  background: var(--panel2);
 
400
  font-size: 10px;
401
  color: var(--muted);
402
  line-height: 1.5;
403
+ margin: 0;
404
  }
405
 
406
  .model-info {
 
410
  font-style: italic;
411
  }
412
 
 
413
  .layer-list {
414
  display: flex;
415
  flex-direction: column;
 
425
  border-radius: 4px;
426
  border: 1px solid var(--border);
427
  position: relative;
428
+ cursor: pointer;
429
  }
430
 
431
  .layer-item.active {
 
439
  background: #000;
440
  object-fit: cover;
441
  border: 1px solid var(--border);
442
+ border-radius: 2px;
443
  }
444
 
445
  .layer-info {
 
472
  color: var(--text);
473
  }
474
 
475
+ .layer-opacity {
476
+ width: 40px;
477
+ height: 3px;
478
+ }
479
+
480
  .drop-zone {
481
  position: absolute;
482
  inset: 0;
 
533
  pointer-events: none;
534
  z-index: 100;
535
  white-space: nowrap;
536
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
537
  }
538
 
539
  #toast.on {
 
574
  color: var(--accent);
575
  }
576
 
 
577
  .modal {
578
  position: absolute;
579
+ top: 0;
580
+ left: 0;
581
+ width: 100%;
582
+ height: 100%;
583
+ background: rgba(0, 0, 0, 0.7);
584
  z-index: 50;
585
  display: none;
586
  align-items: center;
587
  justify-content: center;
588
  }
589
+
590
  .modal-content {
591
  background: var(--panel);
592
  padding: 20px;
 
594
  border-radius: var(--radius);
595
  width: 300px;
596
  }
597
+
598
+ .modal-content h3 {
599
+ margin: 0 0 10px 0;
600
+ font-family: var(--display);
601
+ font-size: 14px;
602
+ }
603
+
604
+ .modal-content input[type="text"] {
605
+ width: 100%;
606
+ background: var(--panel2);
607
+ border: 1px solid var(--border);
608
+ color: var(--text);
609
+ padding: 8px;
610
+ margin-bottom: 10px;
611
+ font-family: var(--mono);
612
+ border-radius: var(--radius);
613
+ }
614
+
615
+ .modal-content button {
616
+ width: 100%;
617
+ margin-bottom: 5px;
618
+ }
619
+
620
+ .progress-bar {
621
+ width: 100%;
622
+ height: 4px;
623
+ background: var(--border);
624
+ border-radius: 2px;
625
+ margin-top: 8px;
626
+ overflow: hidden;
627
+ }
628
+
629
+ .progress-fill {
630
+ height: 100%;
631
+ background: var(--accent);
632
+ width: 0%;
633
+ transition: width 0.3s;
634
+ }
635
+
636
+ .color-presets {
637
+ display: flex;
638
+ gap: 4px;
639
+ flex-wrap: wrap;
640
+ margin-top: 4px;
641
+ }
642
+
643
+ .color-preset {
644
+ width: 20px;
645
+ height: 20px;
646
+ border-radius: 3px;
647
+ cursor: pointer;
648
+ border: 1px solid var(--border);
649
+ }
650
+
651
+ .color-preset:hover {
652
+ border-color: var(--accent);
653
+ }
654
+
655
+ .brush-preview {
656
+ width: 100%;
657
+ height: 40px;
658
+ background: var(--panel2);
659
+ border: 1px solid var(--border);
660
+ border-radius: var(--radius);
661
+ display: flex;
662
+ align-items: center;
663
+ justify-content: center;
664
+ margin-bottom: 8px;
665
+ }
666
+
667
+ .brush-preview canvas {
668
+ border-radius: 50%;
669
+ }
670
+
671
+ .tool-options {
672
+ display: flex;
673
+ gap: 6px;
674
+ margin-bottom: 12px;
675
+ }
676
+
677
+ .tool-option-btn {
678
+ flex: 1;
679
+ padding: 6px;
680
+ font-size: 9px;
681
+ }
682
+
683
+ .tool-option-btn.active {
684
+ background: var(--accent);
685
+ color: #fff;
686
+ border-color: var(--accent);
687
+ }
688
+
689
+ .shortcut-hint {
690
+ position: absolute;
691
+ right: 5px;
692
+ top: 50%;
693
+ transform: translateY(-50%);
694
+ font-size: 8px;
695
+ color: var(--muted);
696
+ font-family: var(--mono);
697
+ }
698
+
699
+ .status-text {
700
+ font-size: 9px;
701
+ color: var(--muted);
702
+ margin-top: 5px;
703
+ min-height: 12px;
704
+ }
705
+
706
+ .image-info {
707
+ display: flex;
708
+ gap: 10px;
709
+ font-size: 9px;
710
+ color: var(--muted);
711
+ margin-bottom: 10px;
712
+ padding: 6px;
713
+ background: var(--panel2);
714
+ border-radius: var(--radius);
715
+ }
716
+
717
+ .info-item {
718
+ display: flex;
719
+ flex-direction: column;
720
+ }
721
+
722
+ .info-item span:first-child {
723
+ color: var(--text);
724
+ font-weight: 500;
725
+ }
726
  </style>
727
  </head>
728
 
 
733
  <div class="logo">perchance <span>//</span> img.edit</div>
734
  <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
735
  <div class="header-actions">
736
+ <button onclick="undo()" title="Undo (Ctrl+Z)"><i class="ph ph-arrow-u-up-left"></i> Undo</button>
737
+ <button onclick="redo()" title="Redo (Ctrl+Y)"><i class="ph ph-arrow-u-up-right"></i> Redo</button>
738
+ <button class="btn-primary" onclick="exportImage()" title="Export"><i class="ph ph-download"></i> Export</button>
739
  </div>
740
  </header>
741
 
 
747
  <div class="label-caps">Tools</div>
748
  <div class="tool-grid">
749
  <button class="tool-btn active" onclick="setTool('move')" title="Pan/Select">
750
+ <i class="ph ph-cursor"></i> Move
751
  </button>
752
  <button class="tool-btn" onclick="setTool('draw')" title="Brush">
753
+ <i class="ph ph-paint-brush"></i> Draw
754
  </button>
755
  <button class="tool-btn" onclick="setTool('erase')" title="Eraser">
756
+ <i class="ph ph-eraser"></i> Erase
757
  </button>
758
  <button class="tool-btn" onclick="setTool('text')" title="Add Text">
759
+ <i class="ph ph-text-t"></i> Text
760
  </button>
761
  <button class="tool-btn" onclick="setTool('rect')" title="Rectangle">
762
+ <i class="ph ph-rectangle"></i> Rect
763
  </button>
764
+ <button class="tool-btn" onclick="setTool('circle')" title="Circle">
765
+ <i class="ph ph-circle"></i> Circle
766
+ </button>
767
+ <button class="tool-btn" onclick="setTool('line')" title="Line">
768
+ <i class="ph ph-line-segment"></i> Line
769
+ </button>
770
+ <button class="tool-btn" onclick="setTool('fill')" title="Fill">
771
+ <i class="ph ph-drop"></i> Fill
772
  </button>
773
  </div>
774
 
775
+ <div class="label-caps">Brush Settings</div>
776
+ <div class="brush-preview">
777
+ <canvas id="brushPreview" width="30" height="30"></canvas>
778
+ </div>
779
  <div class="frow">
780
+ <div class="flabel"><span>Size</span><span class="fval" id="bSizeVal">10px</span></div>
781
+ <input type="range" id="brushSize" min="1" max="100" value="10" oninput="updateBrush()">
782
+ </div>
783
+ <div class="frow">
784
+ <div class="flabel"><span>Hardness</span><span class="fval" id="bHardVal">100%</span></div>
785
+ <input type="range" id="brushHardness" min="0" max="100" value="100" oninput="updateBrush()">
786
  </div>
787
  <div class="frow">
788
  <div class="flabel"><span>Opacity</span><span class="fval" id="bOpVal">100%</span></div>
 
790
  </div>
791
  <div class="frow">
792
  <div class="flabel"><span>Color</span></div>
793
+ <input type="color" id="brushColor" value="#ffffff" oninput="updateBrush()">
794
+ </div>
795
+ <div class="color-presets">
796
+ <div class="color-preset" style="background:#fff" onclick="setColor('#ffffff')"></div>
797
+ <div class="color-preset" style="background:#000" onclick="setColor('#000000')"></div>
798
+ <div class="color-preset" style="background:#f76f6f" onclick="setColor('#f76f6f')"></div>
799
+ <div class="color-preset" style="background:#f7a26f" onclick="setColor('#f7a26f')"></div>
800
+ <div class="color-preset" style="background:#5de8a0" onclick="setColor('#5de8a0')"></div>
801
+ <div class="color-preset" style="background:#7c6ff7" onclick="setColor('#7c6ff7')"></div>
802
  </div>
803
+
804
+ <div class="label-caps">Text Settings</div>
805
  <div class="frow">
806
  <div class="flabel"><span>Font Size</span><span class="fval" id="fontSizeVal">24px</span></div>
807
+ <input type="range" id="fontSize" min="10" max="200" value="24" oninput="updateTextSettings()">
808
+ </div>
809
+ <div class="frow">
810
+ <div class="flabel"><span>Font</span></div>
811
+ <select id="fontFamily" onchange="updateTextSettings()">
812
+ <option value="DM Mono">DM Mono</option>
813
+ <option value="Syne">Syne</option>
814
+ <option value="Arial">Arial</option>
815
+ <option value="Times New Roman">Times New Roman</option>
816
+ <option value="Courier New">Courier New</option>
817
+ </select>
818
+ </div>
819
+ <div class="frow">
820
+ <div class="flabel"><span>Style</span></div>
821
+ <select id="fontStyle" onchange="updateTextSettings()">
822
+ <option value="normal">Normal</option>
823
+ <option value="bold">Bold</option>
824
+ <option value="italic">Italic</option>
825
+ </select>
826
  </div>
 
827
  </div>
828
 
829
  <!-- CANVAS -->
830
  <div class="canvas-area" id="canvasArea" oncontextmenu="return false;">
831
  <div class="drop-zone" id="dropZone">
832
+ <div class="dz-ring"><i class="ph ph-image"></i></div>
833
  <div style="font-family:var(--display);font-weight:700;">Drop Image</div>
834
  <button class="btn-primary" onclick="document.getElementById('fileInput').click()">Browse Files</button>
835
  </div>
836
 
837
  <div class="canvas-centerer">
838
  <div class="canvas-wrap" id="canvasWrap">
 
839
  <canvas id="mainCanvas"></canvas>
840
  </div>
841
  </div>
842
 
843
  <div class="zoom-bar" id="zoomBar">
844
+ <button onclick="zoomOut()">-</button>
845
  <span id="zoomLabel">100%</span>
846
+ <button onclick="zoomIn()">+</button>
847
  <button onclick="fitCanvas()">Fit</button>
848
+ <button onclick="zoom1()">1:1</button>
849
  </div>
850
  </div>
851
 
852
  <!-- RIGHT PANEL -->
853
  <div class="panel-right">
854
  <div class="tab-row">
855
+ <button class="tab active" onclick="switchTab('layers')">Layers</button>
856
+ <button class="tab" onclick="switchTab('adjust')">Adjust</button>
857
+ <button class="tab" onclick="switchTab('upscale')">Upscale</button>
858
+ <button class="tab" onclick="switchTab('bg')">Remove BG</button>
859
+ <button class="tab" onclick="switchTab('crop')">Crop</button>
860
  </div>
861
 
862
  <!-- Layers Tab -->
863
  <div class="tab-panel active" id="tab-layers">
864
+ <div class="image-info" id="imageInfo" style="display:none;">
865
+ <div class="info-item">
866
+ <span>Width</span>
867
+ <span id="imgWidth">0px</span>
868
+ </div>
869
+ <div class="info-item">
870
+ <span>Height</span>
871
+ <span id="imgHeight">0px</span>
872
+ </div>
873
  </div>
874
+ <div class="label-caps">Layer Stack</div>
875
+ <div style="margin-bottom:10px;display:flex;gap:6px;">
876
+ <button style="flex:1" onclick="addNewLayer()">+ New Layer</button>
877
+ <button onclick="duplicateLayer()"><i class="ph ph-copy"></i></button>
878
  </div>
879
+ <div class="layer-list" id="layerList"></div>
880
  </div>
881
 
882
  <!-- Adjust Tab -->
 
902
  <div class="flabel"><span>Hue Rotate</span><span class="fval" id="val-hue">0deg</span></div>
903
  <input type="range" id="hue" min="-180" max="180" value="0" oninput="applyFilters()">
904
  </div>
905
+ <div class="frow">
906
+ <div class="flabel"><span>Invert</span><span class="fval" id="val-invert">0%</span></div>
907
+ <input type="range" id="invert" min="0" max="100" value="0" oninput="applyFilters()">
908
+ </div>
909
  <div class="frow">
910
  <div class="flabel"><span>Presets</span></div>
911
  <select onchange="applyPreset(this.value)">
 
914
  <option value="cool">Cool Tone</option>
915
  <option value="bw">Black & White</option>
916
  <option value="vintage">Vintage</option>
917
+ <option value="sepia">Sepia</option>
918
+ <option value="vibrant">Vibrant</option>
919
+ <option value="dramatic">Dramatic</option>
920
  </select>
921
  </div>
922
  <button class="btn" style="width:100%" onclick="resetFilters()">Reset All</button>
 
954
  </div>
955
 
956
  <button class="btn-primary" style="width:100%" onclick="runUpscale()">Run Upscale</button>
957
+ <div id="upscaleStatus" class="status-text"></div>
958
+ <div class="progress-bar" id="upscaleProgress" style="display:none;">
959
+ <div class="progress-fill" id="upscaleProgressFill"></div>
960
+ </div>
961
  </div>
962
 
963
  <!-- Remove BG Tab -->
964
  <div class="tab-panel" id="tab-bg">
965
  <div class="label-caps">Background Removal</div>
966
  <div class="ai-card">
967
+ <p>Uses @imgly/background-removal. Runs locally in browser.</p>
968
  </div>
969
  <button class="btn-primary" style="width:100%" onclick="removeBackground()">Remove Background</button>
970
+ <div id="bgStatus" class="status-text"></div>
971
+ <div class="progress-bar" id="bgProgress" style="display:none;">
972
+ <div class="progress-fill" id="bgProgressFill"></div>
973
+ </div>
974
+ </div>
975
+
976
+ <!-- Crop Tab -->
977
+ <div class="tab-panel" id="tab-crop">
978
+ <div class="label-caps">Crop & Transform</div>
979
+ <div class="frow">
980
+ <div class="flabel"><span>Width</span><span class="fval" id="cropW">0</span></div>
981
+ <input type="range" id="cropWidth" min="1" max="100" value="100" oninput="updateCropPreview()">
982
+ </div>
983
+ <div class="frow">
984
+ <div class="flabel"><span>Height</span><span class="fval" id="cropH">0</span></div>
985
+ <input type="range" id="cropHeight" min="1" max="100" value="100" oninput="updateCropPreview()">
986
+ </div>
987
+ <div class="tool-options">
988
+ <button class="tool-option-btn active" onclick="setCropAnchor('tl')">TL</button>
989
+ <button class="tool-option-btn" onclick="setCropAnchor('tc')">TC</button>
990
+ <button class="tool-option-btn" onclick="setCropAnchor('tr')">TR</button>
991
+ <button class="tool-option-btn" onclick="setCropAnchor('ml')">ML</button>
992
+ <button class="tool-option-btn" onclick="setCropAnchor('mc')">MC</button>
993
+ <button class="tool-option-btn" onclick="setCropAnchor('mr')">MR</button>
994
+ <button class="tool-option-btn" onclick="setCropAnchor('bl')">BL</button>
995
+ <button class="tool-option-btn" onclick="setCropAnchor('bc')">BC</button>
996
+ <button class="tool-option-btn" onclick="setCropAnchor('br')">BR</button>
997
+ </div>
998
+ <button class="btn-primary" style="width:100%" onclick="applyCrop()">Apply Crop</button>
999
+ <button style="width:100%;margin-top:8px;" onclick="rotateCanvas(-90)">Rotate -90°</button>
1000
+ <button style="width:100%;margin-top:4px;" onclick="rotateCanvas(90)">Rotate +90°</button>
1001
+ <button style="width:100%;margin-top:4px;" onclick="flipHorizontal()">Flip Horizontal</button>
1002
+ <button style="width:100%;margin-top:4px;" onclick="flipVertical()">Flip Vertical</button>
1003
  </div>
1004
  </div>
1005
  </div>
 
1007
  <!-- MODALS -->
1008
  <div class="modal" id="textModal">
1009
  <div class="modal-content">
1010
+ <h3>Add Text</h3>
1011
+ <input type="text" id="textInput" placeholder="Enter text...">
1012
+ <button class="btn-primary" onclick="addTextToCanvas()">Add Text</button>
1013
+ <button style="margin-top:5px;" onclick="closeModal('textModal'); setTool('move')">Cancel</button>
1014
  </div>
1015
  </div>
1016
 
 
1020
  <script>
1021
  // --- UTILS ---
1022
  const getEl = id => document.getElementById(id);
1023
+ const toast = (msg, duration = 3000) => {
1024
  const t = getEl('toast');
1025
  t.textContent = msg;
1026
  t.classList.add('on');
1027
+ setTimeout(() => t.classList.remove('on'), duration);
1028
  };
1029
 
1030
+ // --- STATE ---
1031
+ let layers = [];
1032
+ let activeLayerId = null;
1033
+ let zoomLevel = 1;
1034
+ let panStart = null;
1035
+ let panScroll = null;
1036
+ let isPanning = false;
1037
+ let currentTool = 'move';
1038
+ let isDrawing = false;
1039
+ let drawingLayer = null;
1040
+ let shapeStart = null;
1041
+ let lastPoint = null;
1042
+ let cropAnchor = 'tl';
1043
+
1044
+ // --- HISTORY ---
1045
  const history = {
1046
  stack: [],
1047
  pointer: -1,
1048
+ maxSize: 50,
1049
  push: function(state) {
1050
+ if (this.pointer < this.stack.length - 1) {
1051
+ this.stack = this.stack.slice(0, this.pointer + 1);
1052
+ }
1053
+ this.stack.push(JSON.stringify(state));
1054
+ if (this.stack.length > this.maxSize) {
1055
+ this.stack.shift();
1056
+ }
1057
+ this.pointer = this.stack.length - 1;
1058
  },
1059
  undo: function() {
1060
  if (this.pointer > 0) {
1061
  this.pointer--;
1062
+ restoreState(JSON.parse(this.stack[this.pointer]));
1063
  toast('Undo');
1064
  }
1065
  },
1066
  redo: function() {
1067
  if (this.pointer < this.stack.length - 1) {
1068
  this.pointer++;
1069
+ restoreState(JSON.parse(this.stack[this.pointer]));
1070
  toast('Redo');
1071
  }
1072
  }
1073
  };
1074
 
1075
  function saveState() {
 
1076
  const state = layers.map(l => ({
1077
  id: l.id,
1078
  type: l.type,
1079
+ name: l.name,
1080
+ data: l.canvas.toDataURL('image/png'),
1081
+ x: l.x,
1082
+ y: l.y,
1083
  visible: l.visible,
1084
+ opacity: l.opacity
 
1085
  }));
1086
  history.push(state);
1087
  }
1088
 
1089
  function restoreState(state) {
1090
+ const loadPromises = state.map(l => {
1091
+ return new Promise((resolve) => {
1092
+ const img = new Image();
1093
+ img.onload = () => {
1094
+ const canvas = document.createElement('canvas');
1095
+ canvas.width = img.width;
1096
+ canvas.height = img.height;
1097
+ const ctx = canvas.getContext('2d');
1098
+ ctx.drawImage(img, 0, 0);
1099
+ resolve({
1100
+ id: l.id,
1101
+ type: l.type,
1102
+ name: l.name,
1103
+ canvas: canvas,
1104
+ x: l.x,
1105
+ y: l.y,
1106
+ visible: l.visible,
1107
+ opacity: l.opacity
1108
+ });
1109
+ };
1110
+ img.src = l.data;
1111
+ });
1112
  });
 
 
 
 
1113
 
1114
+ Promise.all(loadPromises).then(loadedLayers => {
1115
+ layers = loadedLayers;
1116
+ activeLayerId = layers[0]?.id || null;
1117
+ renderLayersList();
1118
+ renderLayers();
1119
+ updateImageInfo();
1120
+ });
1121
+ }
 
 
 
1122
 
1123
  // --- INIT ---
1124
  function init() {
 
1125
  getEl('canvasArea').addEventListener('contextmenu', e => e.preventDefault());
1126
 
1127
+ // Pan & Draw handlers
1128
+ getEl('canvasArea').addEventListener('mousedown', handleMouseDown);
1129
+ getEl('canvasArea').addEventListener('mousemove', handleMouseMove);
1130
+ getEl('canvasArea').addEventListener('mouseup', handleMouseUp);
1131
+ getEl('canvasArea').addEventListener('mouseleave', handleMouseUp);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1132
 
1133
+ // Wheel zoom
1134
+ getEl('canvasArea').addEventListener('wheel', handleWheel);
 
 
 
 
1135
 
1136
+ // Drop zone
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
  window.addEventListener('dragover', e => e.preventDefault());
1138
  window.addEventListener('drop', e => {
1139
  e.preventDefault();
1140
  if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]);
1141
  });
1142
 
1143
+ // Keyboard
1144
+ window.addEventListener('keydown', handleKeyDown);
 
 
 
1145
 
1146
  updateBrush();
1147
  updateUpscaleInfo();
1148
+ updateTextSettings();
1149
  }
1150
 
1151
+ function handleMouseDown(e) {
1152
+ if (e.button === 1 || (currentTool === 'move' && e.button === 0)) {
1153
+ e.preventDefault();
1154
+ isPanning = true;
1155
+ panStart = { x: e.clientX, y: e.clientY };
1156
+ panScroll = { x: getEl('canvasArea').scrollLeft, y: getEl('canvasArea').scrollTop };
1157
+ getEl('canvasArea').style.cursor = 'grabbing';
1158
+ } else if (e.button === 0 && activeLayerId) {
1159
+ if (currentTool === 'draw' || currentTool === 'erase') {
1160
+ startDrawing(e);
1161
+ } else if (currentTool === 'rect' || currentTool === 'circle' || currentTool === 'line') {
1162
+ startShape(e);
1163
+ } else if (currentTool === 'fill') {
1164
+ floodFill(e);
1165
+ } else if (currentTool === 'text') {
1166
+ showTextModal(e);
1167
+ }
1168
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
  }
1170
 
1171
+ function handleMouseMove(e) {
1172
+ if (isPanning && panStart && panScroll) {
1173
+ getEl('canvasArea').scrollLeft = panScroll.x - (e.clientX - panStart.x);
1174
+ getEl('canvasArea').scrollTop = panScroll.y - (e.clientY - panStart.y);
 
 
1175
  }
 
1176
 
1177
+ if (isDrawing && drawingLayer) {
1178
+ const pt = getMousePos(drawingLayer.canvas, e);
1179
+ const ctx = drawingLayer.canvas.getContext('2d');
1180
+
1181
+ if (currentTool === 'draw') {
1182
+ ctx.lineCap = 'round';
1183
+ ctx.lineJoin = 'round';
1184
+ ctx.strokeStyle = getEl('brushColor').value;
1185
+ ctx.lineWidth = getEl('brushSize').value;
1186
+ ctx.globalAlpha = getEl('brushOpacity').value / 100;
1187
+
1188
+ if (lastPoint) {
1189
+ ctx.beginPath();
1190
+ ctx.moveTo(lastPoint.x, lastPoint.y);
1191
+ ctx.lineTo(pt.x, pt.y);
1192
+ ctx.stroke();
1193
+ }
1194
+ lastPoint = pt;
1195
+ } else if (currentTool === 'erase') {
1196
+ ctx.globalCompositeOperation = 'destination-out';
1197
+ ctx.lineCap = 'round';
1198
+ ctx.lineJoin = 'round';
1199
+ ctx.lineWidth = getEl('brushSize').value;
1200
+
1201
+ if (lastPoint) {
1202
+ ctx.beginPath();
1203
+ ctx.moveTo(lastPoint.x, lastPoint.y);
1204
+ ctx.lineTo(pt.x, pt.y);
1205
+ ctx.stroke();
1206
+ }
1207
+ lastPoint = pt;
1208
+ ctx.globalCompositeOperation = 'source-over';
1209
+ }
1210
  renderLayers();
1211
  }
1212
  }
1213
 
1214
+ function handleMouseUp() {
1215
+ if (isPanning) {
1216
+ isPanning = false;
1217
+ panStart = null;
1218
+ panScroll = null;
1219
+ get