Add miniPaint as new frontend base

This commit is contained in:
motion
2026-01-26 12:20:16 -05:00
parent bb859105ff
commit 6ae4cbcb77
306 changed files with 60729 additions and 4050 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-transform-runtime", {
"regenerator": true
}]
]
}
+17
View File
@@ -0,0 +1,17 @@
# OS generated files #
.DS_Store
.DS_Store?
._*
.Trashes
ehthumbs.db
Thumbs.db
nbproject/
.idea/
.git/
.vscode
/.project
*.log
/node_modules/
*.js.ignore
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) ViliusL
https://github.com/viliusle
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+60
View File
@@ -0,0 +1,60 @@
# miniPaint
Online image editor lets you create and edit images using HTML5 technologies. No need to buy, download, install, or have outdated flash. No ads. Key features: layers, filters, open source Photoshop alternative.
miniPaint operates directly in the browser. You can create images by pasting from the clipboard (ctrl+v) or uploading from the computer (_using menu or drag & drop_). Nothing will be sent to any server. Everything stays in your browser.
## URL:
**https://viliusle.github.io/miniPaint/**
## Preview:
![miniPaint](https://raw.githubusercontent.com/viliusle/miniPaint/master/images/preview.gif)
(generated using miniPaint)
**Change log:** [/miniPaint/releases](https://github.com/viliusle/miniPaint/releases)
## Browser Support
- Chrome
- Firefox
- Opera
- Edge
- Safari
- Yandex
## Features
**Files**: open images, directories, URLs, data URLs, drag and drop, save (PNG, JPG, BMP, WEBP, animated GIF, TIFF, JSON (layers data), print.
**Edit**: undo, cut, copy, paste, selection, paste from the clipboard.
**Image**: information, EXIF, trim, zoom, resize (Hermite resample, default resize), rotate, flip, color corrections (brightness, contrast, hue, saturation, luminance), automatic color adjustment, grid, histogram, negative.
**Layers**: multi-layer system, differences, merging, flattening, transparency support.
**Effects**: black and white, blur (box, gaussian, stack, zoom), bulge/pinch, denoise, desaturation, dither, dot screen, edge, emboss, enrich, gamma, grains, grayscale, heatmap, jpg compression, mosaic, oil, sepia, sharpen, solarize, tilt shift, vignette, vibrance, vintage, blueprint, night vision, pencil, also instagram filters: 1977, aden, clarendon, gingham, inkwell, lo-fi, toaster, valencia, x-pro ii.
**Tools**: pencil, brush, magic wand, eraser, fill, color picker, letter, crop, blur, sharpener, desaturation, clone, borders, sprites, keypoints, color zoom, change color, restore transparency, content fill.
**Help**: keyboard shortcuts, translation.
## Embed
To embed this app on another page, use the following HTML code:
<iframe style="box-sizing:border-box; width:100%; height:100vh;" id="miniPaint" src="https://viliusle.github.io/miniPaint/" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
## Build instructions
See [Wiki > Build instructions](https://github.com/viliusle/miniPaint/wiki/Build-instructions)
## Wiki
See [Wiki](https://github.com/viliusle/miniPaint/wiki)
## Contributors
<a align="center" href="https://github.com/viliusle/miniPaint/graphs/contributors">
<img src="https://contrib.rocks/image?repo=viliusle/miniPaint" />
</a>
## License
MIT License
## Support
Please use the GitHub issues for support, feature requests and bug reports, or contact us by sending an email to www.viliusl@gmail.com.
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| latest | :white_check_mark: |
| < latest | :x: |
## Reporting a Vulnerability
Please send details to www.viliusl@gmail.com
+76
View File
@@ -0,0 +1,76 @@
<html>
<body style="margin:0;">
<iframe id="myFrame" style="width:100%;height:100vh;border:0;" src="../" allow="camera"></iframe>
<script>
window.addEventListener('load', function (e) {
draw_various();
}, false);
async function draw_various() {
var Layers = document.getElementById('myFrame').contentWindow.Layers;
//add layer
var canvas = document.createElement('canvas');
canvas.width = 50;
canvas.height = 50;
var ctx = canvas.getContext("2d");
cross_X(ctx, 25, 25);
var params = {
type: 'image',
data: canvas.toDataURL("image/png"),
x: 50,
y: 50,
width: canvas.width,
height: canvas.height,
};
await Layers.insert(params);
//add new layer
var canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext("2d");
cross_X(ctx, 200, 200, null, '#00cc00');
var params = {
type: 'image',
data: canvas.toDataURL("image/png"),
width: canvas.width,
height: canvas.height,
};
await Layers.insert(params);
//update last layer
var canvas = Layers.convert_layer_to_canvas(null, true); //no trim
var ctx = canvas.getContext("2d");
cross_X(ctx, 300, 300, null, '#00cc00');
Layers.update_layer_image(canvas);
//also update layer
var link = Layers.get_layer();
link.x = parseInt(canvas.dataset.x);
link.y = parseInt(canvas.dataset.y);
link.width = canvas.width;
link.height = canvas.height;
}
function cross_X(ctx, x, y, size = 20, col = '#ff0000') {
if(size == null)
size = 20;
x = parseFloat(x);
y = parseFloat(y);
size = parseInt(size);
var lw = parseInt(5);
ctx.beginPath();
ctx.moveTo(x - size, y - size);
ctx.lineTo(x + size, y + size);
ctx.lineWidth = lw;
ctx.strokeStyle = col;
ctx.stroke();
ctx.moveTo(x + size, y - size);
ctx.lineTo(x - size, y + size);
ctx.lineWidth = lw;
ctx.strokeStyle = col;
ctx.stroke();
}
</script>
+128
View File
@@ -0,0 +1,128 @@
<html>
<body style="margin:0;">
<iframe id="myFrame" style="width:100%;height:70vh;border:0;" src="../" allow="camera"></iframe>
<div style="height:20vh;margin:10px;">
Click on image to edit.
<br /><br />
<button onclick="open_image()">Open image</button>
<button onclick="update_image()">Update image</button>
<button onclick="save_image()">Save image</button>
OR
<button onclick="open_json()">Open JSON</button>
<button onclick="save_json()">Save JSON</button>
<br /><br />
<img style="max-height:100%" id="testImage" alt="" src="../images/logo-colors.png" onclick="open_image(this)" />
<textarea rows="8" style="width:500px;" id="testJson"></textarea>
</div>
<script>
/**
* will open image on minipaint
*
* @param {string|image} image image or image id
*/
function open_image(image){
if(image == undefined)
image = document.getElementById('testImage');
if(typeof image == 'string'){
image = document.getElementById(image);
}
var Layers = document.getElementById('myFrame').contentWindow.Layers;
var name = image.src.replace(/^.*[\\\/]/, '');
var new_layer = {
name: name,
type: 'image',
data: image,
width: image.naturalWidth || image.width,
height: image.naturalHeight || image.height,
width_original: image.naturalWidth || image.width,
height_original: image.naturalHeight || image.height,
};
Layers.insert(new_layer);
}
function open_json(){
var miniPaint = document.getElementById('myFrame').contentWindow;
var miniPaint_FileOpen = miniPaint.FileOpen;
window.fetch("../images/test-collection.json").then(function(response) {
return response.json();
}).then(function(json) {
miniPaint_FileOpen.load_json(json, false);
}).catch(function(ex) {
alert('Sorry, image could not be loaded.');
});
}
function save_image(){
var Layers = document.getElementById('myFrame').contentWindow.Layers;
var tempCanvas = document.createElement("canvas");
var tempCtx = tempCanvas.getContext("2d");
var dim = Layers.get_dimensions();
tempCanvas.width = dim.width;
tempCanvas.height = dim.height;
Layers.convert_layers_to_canvas(tempCtx);
if(is_edge_or_ie() == false){
//update image using blob (faster)
tempCanvas.toBlob(function (blob) {
alert('Data length: ' + blob.size);
console.log(blob);
}, 'image/png');
}
else{
//slow way for IE, Edge
var data = tempCanvas.toDataURL();
alert('Data length: ' + data.length);
console.log(data);
}
}
function save_json(){
var miniPaint = document.getElementById('myFrame').contentWindow;
var miniPaint_FileSave = miniPaint.FileSave;
var data_json = miniPaint_FileSave.export_as_json();
document.getElementById('testJson').value = data_json;
}
function update_image(){
var target = document.getElementById('testImage');
var Layers = document.getElementById('myFrame').contentWindow.Layers;
var tempCanvas = document.createElement("canvas");
var tempCtx = tempCanvas.getContext("2d");
var dim = Layers.get_dimensions();
tempCanvas.width = dim.width;
tempCanvas.height = dim.height;
Layers.convert_layers_to_canvas(tempCtx);
target.width = dim.width;
target.height = dim.height;
target.src = tempCanvas.toDataURL();
}
/**
* will auto load image on page load. Uncomment line below to make it work
*/
window.onload = function () {
//open_image(document.getElementById('testImage')); //uncomment me
};
//if IE 11 or Edge
function is_edge_or_ie() {
//ie11
if( !(window.ActiveXObject) && "ActiveXObject" in window )
return true;
//edge
if( navigator.userAgent.indexOf('Edge/') != -1 )
return true;
return false;
}
</script>
+71
View File
@@ -0,0 +1,71 @@
<html>
<body style="margin:0;">
<iframe id="myFrame" style="width:100%;height:100vh;border:0;" src="../" allow="camera"></iframe>
<script>
window.addEventListener('load', function (e) {
main();
}, false);
async function main() {
var Layers = document.getElementById('myFrame').contentWindow.Layers;
var config = document.getElementById('myFrame').contentWindow.AppConfig;
var canvas_width = 800;
var canvas_height = 600;
//set size
Layers.Base_gui.set_size(canvas_width, canvas_height);
//add rectangle
this.layer = {
name: 'layer1',
type: 'rectangle',
params: {
fill: true,
square: false,
border_size: 2,
border: false,
border_color: "#1b1bd8",
fill_color: "#1b1bd8"
},
color: '#ff0000',
render_function: ['rectangle', 'render'],
x: canvas_width/2 - 25,
y: canvas_height/2 - 25,
width: 50,
height: 50,
};
await Layers.insert(this.layer);
//zoom to 500%
setTimeout(function () {
Layers.Base_gui.GUI_preview.zoom_to_position(0, 0);
Layers.Base_gui.GUI_preview.zoom(500);
}, 100);
//do this after system changed zoom position
setTimeout(function () {
//move visible area to begin of rectangle
Layers.Base_gui.GUI_preview.zoom_to_position(500, 500); //change zoom position
}, 500);
//action after 1 s - for preview purpose
setTimeout(function () {
//move visible area so rect is in center
var visible_area = Layers.Base_gui.get_visible_area_size();
//center of rect
var cx = canvas_width/2;
var cy = canvas_height/2;
//calc needed coords
var x = cx - visible_area.width / 2;
var y = cy - visible_area.height / 2;
Layers.Base_gui.GUI_preview.zoom_to_position(x, y); //change zoom position
}, 1000);
}
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 298.73 298.73" style="enable-background:new 0 0 298.73 298.73;" xml:space="preserve">
<g>
<path style="fill:#010002;" d="M264.959,9.35H33.787C15.153,9.35,0,24.498,0,43.154v212.461c0,18.634,15.153,33.766,33.787,33.766
h231.171c18.634,0,33.771-15.132,33.771-33.766V43.154C298.73,24.498,283.593,9.35,264.959,9.35z M193.174,59.623
c18.02,0,32.634,14.615,32.634,32.634s-14.615,32.634-32.634,32.634c-18.025,0-32.634-14.615-32.634-32.634
S175.149,59.623,193.174,59.623z M254.363,258.149H149.362H49.039c-9.013,0-13.027-6.521-8.964-14.566l56.006-110.93
c4.058-8.044,11.792-8.762,17.269-1.605l56.316,73.596c5.477,7.158,15.05,7.767,21.386,1.354l13.777-13.951
c6.331-6.413,15.659-5.619,20.826,1.762l35.675,50.959C266.487,252.16,263.376,258.149,254.363,258.149z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30.051 30.051" style="enable-background:new 0 0 30.051 30.051;" xml:space="preserve">
<g>
<path d="M19.982,14.438l-6.24-4.536c-0.229-0.166-0.533-0.191-0.784-0.062c-0.253,0.128-0.411,0.388-0.411,0.669v9.069
c0,0.284,0.158,0.543,0.411,0.671c0.107,0.054,0.224,0.081,0.342,0.081c0.154,0,0.31-0.049,0.442-0.146l6.24-4.532
c0.197-0.145,0.312-0.369,0.312-0.607C20.295,14.803,20.177,14.58,19.982,14.438z"/>
<path d="M15.026,0.002C6.726,0.002,0,6.728,0,15.028c0,8.297,6.726,15.021,15.026,15.021c8.298,0,15.025-6.725,15.025-15.021
C30.052,6.728,23.324,0.002,15.026,0.002z M15.026,27.542c-6.912,0-12.516-5.601-12.516-12.514c0-6.91,5.604-12.518,12.516-12.518
c6.911,0,12.514,5.607,12.514,12.518C27.541,21.941,21.937,27.542,15.026,27.542z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="124" height="150" viewBox="0 0 124 150" xmlns="http://www.w3.org/2000/svg">
<rect x="55" y="14" width="14" height="63"/>
<rect x="116" width="14" height="61" transform="rotate(90 116 0)"/>
<path d="M62 150L8.30643 75L115.694 75L62 150Z"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M264.574,4.675C262.697,1.761,259.467,0,256,0c-3.467,0-6.697,1.761-8.574,4.675
c-6.532,10.14-159.966,249.362-159.966,338.784C87.459,436.393,163.066,512,256,512s168.541-75.607,168.541-168.541
C424.541,254.037,271.106,14.815,264.574,4.675z M256,491.602c-81.686,0-148.142-66.456-148.142-148.143
c0-34.037,26.926-101.269,77.865-194.427C213.83,97.626,242.219,51.324,256,29.29c13.77,22.016,42.123,68.259,70.223,119.64
c50.976,93.212,77.92,160.478,77.92,194.529C404.142,425.146,337.686,491.602,256,491.602z"/>
</g>
</g>
<g>
<g>
<path d="M375.907,332.939c-5.633,0-10.199,4.566-10.199,10.199c0,43.197-25.482,82.521-64.919,100.181
c-5.141,2.301-7.442,8.335-5.14,13.476c1.695,3.788,5.416,6.034,9.314,6.034c1.393,0,2.809-0.287,4.163-0.893
c46.764-20.941,76.981-67.572,76.981-118.797C386.106,337.505,381.54,332.939,375.907,332.939z"/>
</g>
</g>
<g>
<g>
<path d="M281.818,460.702c-0.729-5.586-5.85-9.519-11.435-8.791c-4.736,0.619-9.574,0.933-14.383,0.933
c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199c5.69,0,11.419-0.372,17.028-1.106
C278.613,471.407,282.548,466.287,281.818,460.702z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
</svg>

After

Width:  |  Height:  |  Size: 491 B

+1
View File
@@ -0,0 +1 @@
<svg height="443pt" viewBox="0 0 443.06138 443" width="443pt" xmlns="http://www.w3.org/2000/svg"><path d="m431.328125 25.894531-14.136719-14.136719c-8.070312-8.078124-19.210937-12.320312-30.613281-11.6601558-11.402344.6601558-21.976563 6.1640628-29.058594 15.1249998l-117.023437 164.839844c-4.089844 5.761719-10.511719 9.425781-17.554688 10.019531-7.039062.59375-13.984375-1.945312-18.980468-6.941406l-34.234376-34.207031c-9.480468-9.109375-24.46875-9.109375-33.949218 0l-11.296875 11.304687 158.398437 158.398438 11.304688-11.304688c4.527344-4.488281 7.070312-10.597656 7.070312-16.96875 0-6.375-2.542968-12.484375-7.070312-16.972656l-34.222656-34.234375c-4.996094-4.996094-7.535157-11.941406-6.941407-18.980469.59375-7.042969 4.257813-13.464843 10.019531-17.554687l165-117.183594c8.890626-7.109375 14.332032-17.671875 14.960938-29.035156.628906-11.367188-3.617188-22.464844-11.671875-30.507813zm-24 43.808594c-9.375 9.371094-24.570313 9.371094-33.945313 0-9.371093-9.375-9.371093-24.574219 0-33.945313 9.496094-9.0625 24.441407-9.0625 33.9375 0 9.375 9.371094 9.378907 24.570313.007813 33.945313zm0 0"/><path d="m390.351562 44.734375c-3.234374 0-6.152343 1.949219-7.390624 4.9375-1.234376 2.988281-.550782 6.429687 1.734374 8.71875 3.160157 3.03125 8.148438 3.03125 11.304688 0 2.289062-2.289063 2.972656-5.730469 1.734375-8.71875s-4.15625-4.9375-7.390625-4.9375zm0 0"/><path d="m135.792969 420.429688 22.65625 22.65625 113.085937-113.167969-158.398437-158.402344-113.136719 113.121094 22.65625 22.65625 84.847656-84.855469c2.007813-2.078125 4.984375-2.914062 7.78125-2.183594 2.796875.734375 4.980469 2.917969 5.710938 5.714844.734375 2.796875-.101563 5.773438-2.179688 7.78125l-84.847656 84.855469 22.632812 22.625 50.902344-50.90625c3.140625-3.03125 8.128906-2.988281 11.214844.097656s3.128906 8.078125.097656 11.214844l-50.90625 50.921875 22.617188 22.621094 62.214844-62.222657c3.125-3.125 8.191406-3.128906 11.316406-.003906 3.128906 3.125 3.128906 8.191406.003906 11.316406l-62.222656 62.222657 22.625 22.625 73.535156-73.535157c3.140625-3.03125 8.128906-2.988281 11.214844.097657 3.085937 3.085937 3.128906 8.074218.097656 11.214843zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 402.56 402.56" style="enable-background:new 0 0 402.56 402.56;" xml:space="preserve">
<g>
<g>
<polygon points="38.613,234.88 38.4,274.56 96.64,274.56 116.693,265.173 9.6,372.48 39.68,402.56 147.2,295.253 137.813,316.587
137.813,373.76 177.493,374.187 177.493,234.88 "/>
</g>
</g>
<g>
<g>
<polygon points="306.987,128.213 285.653,137.6 392.96,30.08 362.88,0 255.573,107.307 264.96,87.04 264.96,28.8 225.28,29.013
225.28,167.893 364.587,167.893 364.16,128.213 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 778 B

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 540.721 540.721" style="enable-background:new 0 0 540.721 540.721;" xml:space="preserve">
<g>
<g>
<path d="M521.858,465.271H18.862c-7.545,0-12.575,5.03-12.575,12.575v50.3c0,7.545,5.03,12.575,12.575,12.575h502.996
c7.545,0,12.575-5.03,12.575-12.575v-50.3C534.433,470.301,529.403,465.271,521.858,465.271z"/>
<path d="M227.606,98.084c5.03-5.03,20.12-12.575,30.18-12.575c7.545,0,12.575-5.03,12.575-12.575c0-7.545-5.03-10.06-12.575-10.06
l0,0c-17.605,0-37.725,7.545-47.785,17.605c-12.575,10.06-22.635,27.665-22.635,47.785c0,7.545,5.03,10.06,12.575,10.06l0,0
c7.545,0,12.575-2.515,12.575-10.06C212.516,118.204,220.061,103.114,227.606,98.084z"/>
<path d="M18.862,440.121h502.996c5.03,0,7.545-2.515,10.06-5.03c2.515-2.515,2.515-7.545,0-12.575l-47.785-100.599
c0-5.03-5.03-7.545-10.06-7.545H333.235v-77.964c25.15-20.12,60.359-60.36,60.359-110.659C393.594,55.33,338.265,0,270.36,0
S147.126,57.845,147.126,125.749c-2.515,47.785,35.21,88.024,60.36,110.659v77.964H66.647c-5.03,0-10.06,2.515-10.06,7.545
L8.802,422.517c-2.515,5.03-2.515,7.545,0,12.575C11.317,440.121,13.832,440.121,18.862,440.121z M169.761,125.749
c0-55.33,45.27-100.599,98.084-100.599c52.815,0,98.084,45.27,98.084,100.599c0,37.725-27.665,75.449-55.33,93.054
c-2.515,2.515-5.03,5.03-5.03,10.06v123.234c0,2.515-12.575,10.06-22.635,10.06h-37.725c-7.545,0-15.09-7.545-15.09-10.06V228.863
c0-5.03-2.515-7.545-5.03-10.06C207.486,206.228,169.761,168.504,169.761,125.749z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="956.815px" height="956.815px" viewBox="0 0 956.815 956.815" style="enable-background:new 0 0 956.815 956.815;"
xml:space="preserve">
<g>
<path d="M137.621,162.622H20c-11.046,0-20,8.954-20,20v72.919c0,11.046,8.954,20,20,20h117.622L137.621,162.622L137.621,162.622z"
/>
<path d="M774.193,956.815c11.046,0,20-8.954,20-20V819.193H681.274v117.621c0,11.046,8.954,20,20,20L774.193,956.815
L774.193,956.815z"/>
<path d="M794.193,656.275V182.622c0-11.046-8.954-20-20-20H300.54v112.919h380.734v380.734H794.193z"/>
<path d="M936.814,681.275H794.193H681.274H275.54V275.541V162.622V20c0-11.046-8.954-20-20-20h-72.918c-11.046,0-20,8.954-20,20
v142.622v112.919v498.653c0,11.046,8.954,20,20,20h498.653h112.918h142.622c11.045,0,20-8.954,20-20v-72.918
C956.814,690.229,947.86,681.275,936.814,681.275z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg height="365pt" viewBox="0 0 365.71733 365" width="365pt" xmlns="http://www.w3.org/2000/svg">
<g fill="#f44336">
<path d="m356.339844 296.347656-286.613282-286.613281c-12.5-12.5-32.765624-12.5-45.246093 0l-15.105469 15.082031c-12.5 12.503906-12.5 32.769532 0 45.25l286.613281 286.613282c12.503907 12.5 32.769531 12.5 45.25 0l15.082031-15.082032c12.523438-12.480468 12.523438-32.75.019532-45.25zm0 0"/>
<path d="m295.988281 9.734375-286.613281 286.613281c-12.5 12.5-12.5 32.769532 0 45.25l15.082031 15.082032c12.503907 12.5 32.769531 12.5 45.25 0l286.632813-286.59375c12.503906-12.5 12.503906-32.765626 0-45.246094l-15.082032-15.082032c-12.5-12.523437-32.765624-12.523437-45.269531-.023437zm0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 719 B

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path d="M2646.5,3789.8c-1040.2-119.8-2106-953.5-2445-1917.3l-91.8-260.1L102,355.5c-5.1-1081-2.5-1279.9,33.1-1415c155.5-609.4,578.8-986.7,1282.4-1139.7c300.9-66.3,670.5-96.9,1534.8-127.5c400.3-12.7,831.2-43.3,961.2-63.7c464-76.5,836.3-229.5,1448.1-599.2c698.6-420.7,1063.2-550.7,1682.7-599.2c234.6-17.9,645,20.4,866.9,81.6c494.6,132.6,1007.1,476.8,1341.1,902.6c318.7,405.4,484.4,749.6,589,1223.8c53.5,255,56.1,313.6,58.6,1527.2c0,1155-5.1,1274.8-45.9,1415c-186.1,596.6-609.4,917.9-1374.2,1045.3c-135.1,22.9-550.7,51-963.7,63.7c-1560.3,53.5-1828,112.2-2957.5,662.9c-685.8,334-928,415.6-1328.3,458.9C2944.8,3820.4,2919.3,3820.4,2646.5,3789.8z M3378.2,3165.2c255-51,461.5-135.1,1014.7-413c543.1-270.3,821-382.4,1213.6-487c311-84.2,798-155.5,1042.8-155.5c142.8,0,158.1-5.1,234.6-91.8c112.2-124.9,214.2-163.2,420.7-163.2c193.8,0,359.5,61.2,423.2,153c40.8,56.1,48.5,58.6,328.9,43.3c894.9-51,1249.3-328.9,1246.7-971.4c0-117.3-7.6-247.3-17.8-288.1c-30.6-147.9-94.3-331.4-168.3-494.6l-76.5-168.3l-158.1-12.7c-379.9-30.6-563.4-270.3-372.2-487l73.9-86.7l-81.6-76.5c-211.6-198.9-527.8-359.5-843.9-428.3c-160.6-35.7-272.8-43.3-560.9-30.6c-555.8,20.4-777.6,99.4-1494,525.2C5111.9-174.8,4857-52.4,4482.2,70c-410.5,137.7-685.8,175.9-1402.3,206.5c-1287.5,51-1596,86.7-1891.8,219.3c-191.2,84.1-379.9,288.1-443.6,471.7c-66.3,193.8-58.6,471.7,17.9,696l61.2,175.9l155.5,5.1c226.9,5.1,402.8,119.8,433.4,280.5c20.4,107.1,0,168.3-84.1,249.9l-66.3,66.3l61.2,66.3c249.9,267.7,808.2,571.1,1200.8,657.8C2745.9,3213.6,3143.6,3213.6,3378.2,3165.2z M3391-391.5c221.8-112.2,397.7-351.8,362-492.1c-51-211.6-418.1-183.6-657.8,51C2972.8-712.8,2921.8-610.8,2932-496c5.1,79.1,22.9,102,96.9,137.7C3130.9-307.4,3240.5-317.6,3391-391.5z M1328.3-414.5c140.2-86.7,221.8-209.1,221.8-323.8c0-216.7-392.6-153-573.7,91.8C792.9-394.1,1040.2-230.9,1328.3-414.5z M5550.4-1273.7c135.1-84.1,244.8-257.5,221.8-349.3c-23-89.2-145.3-132.6-267.7-96.9c-137.7,43.4-293.2,163.2-341.7,267.7c-56.1,117.3-53.5,160.6,10.2,224.4C5244.5-1156.4,5387.3-1174.2,5550.4-1273.7z M2682.2-1339.9c293.2-165.7,341.6-476.8,73.9-476.8c-196.3,0-466.6,232-466.6,397.8C2289.5-1278.7,2503.7-1238,2682.2-1339.9z M6659.5-2446.5c209-107.1,372.2-308.5,372.2-461.5c0-68.8-79-150.4-163.2-173.4c-168.3-40.8-517.6,158.1-617,351.8C6111.3-2459.2,6353.6-2290.9,6659.5-2446.5z"/><path d="M3013.6,2639.9c-135.1-45.9-221.8-130-221.8-219.3c0-160.6,181-270.3,453.8-270.3c418.1-2.5,599.2,288.1,280.5,453.8C3408.8,2665.4,3138.5,2683.3,3013.6,2639.9z"/><path d="M2213,1490.1c-168.3-48.5-295.8-132.6-334-226.9c-28-68.8-25.5-89.2,17.9-168.3c188.7-351.9,1106.5-288.1,1106.5,79c0,48.5-22.9,114.7-51,150.4C2837.7,1472.2,2450.2,1556.4,2213,1490.1z"/><path d="M5693.2,1492.6c-119.8-40.8-216.7-145.3-216.7-232c0-267.7,594-372.2,846.5-147.9c201.4,181-17.9,410.5-387.5,407.9C5843.6,1520.7,5736.6,1507.9,5693.2,1492.6z"/><path d="M8051.6,1278.5c-124.9-53.5-178.5-109.6-178.5-188.7c0-137.7,165.7-229.5,415.6-229.5c175.9,0,341.7,73.9,377.3,168.3C8737.4,1227.5,8321.8,1395.8,8051.6,1278.5z"/><path d="M4321.5,1013.3c-147.9-28-234.6-102-234.6-204c0-124.9,96.9-186.1,313.6-198.9c204-12.7,346.7,35.7,392.6,132.6C4869.7,916.4,4609.6,1064.3,4321.5,1013.3z"/><path d="M6662.1,225.5c-196.3-48.4-344.2-181-344.2-311c0-323.8,775.1-453.8,1070.8-175.9C7671.7,3.7,7161.8,345.3,6662.1,225.5z"/></g></g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="548.157px" height="548.157px" viewBox="0 0 548.157 548.157" style="enable-background:new 0 0 548.157 548.157;"
xml:space="preserve">
<g>
<path d="M545.027,112.765c-3.046-6.471-7.57-11.657-13.565-15.555c-5.996-3.9-12.614-5.852-19.846-5.852H292.351
c-11.04,0-20.175,4.184-27.408,12.56L9.13,396.279c-4.758,5.328-7.661,11.56-8.708,18.698c-1.049,7.139-0.144,13.941,2.712,20.417
c3.044,6.468,7.564,11.652,13.561,15.553c5.997,3.898,12.612,5.853,19.845,5.853h219.268c11.042,0,20.177-4.179,27.41-12.56
l255.813-292.363c4.75-5.33,7.655-11.561,8.699-18.699C548.788,126.039,547.877,119.238,545.027,112.765z M255.811,420.254H36.54
l95.93-109.632h219.27L255.811,420.254z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<path d="M494.412,245.154L371.59,122.507l93.792-93.934L437.07,0.304L343.28,94.237l-56.613-56.532L37.714,286.659
l207.754,207.459c11.699,11.699,27.069,17.549,42.436,17.549c15.367,0,30.735-5.849,42.436-17.549l164.082-164.082
c11.389-11.388,17.632-26.53,17.578-42.635C511.946,271.446,505.704,256.446,494.412,245.154z M466.131,301.745L302.048,465.828
c-7.799,7.8-20.489,7.8-28.3-0.01L94.314,286.638L286.687,94.266l28.324,28.283L211.272,226.445l28.312,28.269L343.322,150.82
l122.811,122.636c3.761,3.761,5.842,8.761,5.859,14.079C472.009,292.902,469.928,297.948,466.131,301.745z"/>
</g>
<g>
<path d="M95.137,398.966c-10.179-15.198-20.245-27.482-20.669-27.997l-15.455-18.808l-15.455,18.808
c-0.424,0.516-10.49,12.799-20.669,27.997C2.372,429.596,0,444.293,0,452.684c0,32.54,26.474,59.012,59.012,59.012
c32.539,0,59.012-26.473,59.012-59.012C118.025,444.293,115.652,429.596,95.137,398.966z M59.012,471.688
c-10.479,0-19.004-8.525-19.005-18.956c0.004-0.08,0.526-8.271,16.291-31.757c0.907-1.35,1.813-2.678,2.714-3.972
c0.899,1.294,1.806,2.622,2.714,3.972c15.491,23.079,16.265,31.388,16.29,31.745C77.996,463.182,69.48,471.688,59.012,471.688z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-type-italic" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M7.991 11.674L9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/></svg>

After

Width:  |  Height:  |  Size: 393 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g id="XMLID_1_">
<path id="XMLID_12_" d="M128.4,96.2L64.2,32H32.6v31.6l64.2,64.2L128.4,96.2z M160,0.4h31.6v64.2H160V0.4z M287.5,160.4h64.2V192
h-64.2V160.4z M320,64.6V33h-31.6l-64.2,64.2l31.6,31.6L320,64.6z M0,160.4h64.2V192H0V160.4z M160,287.9h31.6v64.2H160V287.9z
M31.6,287.9v31.6h31.6l64.2-64.2l-31.6-31.6L31.6,287.9z M504.2,441.4L187,123.2c-9.3-9.3-24.2-9.3-33.5,0L123.7,153
c-9.3,9.3-9.3,24.2,0,33.5l318.2,318.2c9.3,9.3,24.2,9.3,33.5,0l29.8-29.8C514.4,465.5,514.4,450.7,504.2,441.4z M240,272
l-95.8-95.8l31.6-31.6l95.8,95.8L240,272z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 913 B

+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="548.176px" height="548.176px" viewBox="0 0 548.176 548.176" style="enable-background:new 0 0 548.176 548.176;"
xml:space="preserve">
<g>
<g>
<path d="M534.75,68.238c-8.945-8.945-19.694-13.417-32.261-13.417H45.681c-12.562,0-23.313,4.471-32.264,13.417
C4.471,77.185,0,87.936,0,100.499v347.173c0,12.566,4.471,23.318,13.417,32.264c8.951,8.946,19.702,13.419,32.264,13.419h456.815
c12.56,0,23.312-4.473,32.258-13.419c8.945-8.945,13.422-19.697,13.422-32.264V100.499
C548.176,87.936,543.699,77.185,534.75,68.238z M511.623,447.672c0,2.478-0.899,4.613-2.707,6.427
c-1.81,1.8-3.952,2.703-6.427,2.703H45.681c-2.473,0-4.615-0.903-6.423-2.703c-1.807-1.813-2.712-3.949-2.712-6.427V100.495
c0-2.474,0.902-4.611,2.712-6.423c1.809-1.803,3.951-2.708,6.423-2.708h456.815c2.471,0,4.613,0.905,6.42,2.708
c1.801,1.812,2.707,3.949,2.707,6.423V447.672L511.623,447.672z"/>
<path d="M127.91,237.541c15.229,0,28.171-5.327,38.831-15.987c10.657-10.66,15.987-23.601,15.987-38.826
c0-15.23-5.333-28.171-15.987-38.832c-10.66-10.656-23.603-15.986-38.831-15.986c-15.227,0-28.168,5.33-38.828,15.986
c-10.656,10.66-15.986,23.601-15.986,38.832c0,15.225,5.327,28.169,15.986,38.826C99.742,232.211,112.683,237.541,127.91,237.541z
"/>
<polygon points="210.134,319.765 164.452,274.088 73.092,365.447 73.092,420.267 475.085,420.267 475.085,292.36 356.315,173.587
"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="124px" height="124px" viewBox="0 0 124 124" style="enable-background:new 0 0 124 124;" xml:space="preserve">
<g>
<path d="M112,6H12C5.4,6,0,11.4,0,18s5.4,12,12,12h100c6.6,0,12-5.4,12-12S118.6,6,112,6z"/>
<path d="M112,50H12C5.4,50,0,55.4,0,62c0,6.6,5.4,12,12,12h100c6.6,0,12-5.4,12-12C124,55.4,118.6,50,112,50z"/>
<path d="M112,94H12c-6.6,0-12,5.4-12,12s5.4,12,12,12h100c6.6,0,12-5.4,12-12S118.6,94,112,94z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 797 B

+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<polygon points="51.2,353.28 0,512 158.72,460.8 "/>
</g>
</g>
<g>
<g>
<rect x="89.73" y="169.097" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.8575 260.3719)" width="353.277" height="153.599"/>
</g>
</g>
<g>
<g>
<path d="M504.32,79.36L432.64,7.68c-10.24-10.24-25.6-10.24-35.84,0l-23.04,23.04l107.52,107.52l23.04-23.04
C514.56,104.96,514.56,89.6,504.32,79.36z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 767 B

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 296.135 296.135" style="enable-background:new 0 0 296.135 296.135;" xml:space="preserve">
<path d="M284.5,11.635C276.997,4.132,267.021,0,256.411,0s-20.586,4.132-28.089,11.635l-64.681,64.68l-6.658-6.658
c-2.777-2.777-6.2-4.512-9.786-5.206c-0.598-0.116-1.2-0.202-1.804-0.26s-1.211-0.087-1.817-0.087s-1.213,0.029-1.817,0.087
s-1.206,0.145-1.804,0.26c-3.585,0.694-7.009,2.43-9.786,5.206v0c-1.388,1.388-2.516,2.938-3.384,4.59
c-0.289,0.55-0.55,1.112-0.781,1.683c-0.694,1.712-1.128,3.505-1.302,5.317c-0.058,0.604-0.087,1.211-0.087,1.817
c0,1.213,0.116,2.426,0.347,3.621c0.347,1.793,0.954,3.545,1.822,5.196c0.868,1.651,1.996,3.201,3.384,4.59l4.319,4.319
L21.468,213.811c-1.434,1.434-2.563,3.143-3.316,5.025l-16.19,40.387c-3.326,8.298-2.338,17.648,2.644,25.013
c5.04,7.451,13.356,11.899,22.244,11.899c3.432,0,6.817-0.659,10.063-1.961L77.3,277.984c1.882-0.754,3.592-1.883,5.025-3.316
l113.021-113.021l4.318,4.318c0.463,0.463,0.944,0.897,1.44,1.302c0.993,0.81,2.049,1.504,3.15,2.083
c2.752,1.446,5.785,2.169,8.818,2.169l0,0c0.029,0,0.058-0.004,0.087-0.004c1.791-0.008,3.58-0.264,5.312-0.777
c2.345-0.694,4.583-1.851,6.569-3.471c0.497-0.405,0.977-0.839,1.44-1.302v0c2.314-2.314,3.905-5.077,4.772-8.009
c0.694-2.345,0.926-4.798,0.694-7.216c-0.116-1.209-0.347-2.408-0.694-3.581s-0.81-2.318-1.388-3.419
c-0.868-1.651-1.996-3.201-3.384-4.59l-6.658-6.658l64.68-64.68C299.988,52.326,299.988,27.124,284.5,11.635z M63.285,251.282
l-30.764,12.331l12.332-30.763l110.848-110.848l18.432,18.432L63.285,251.282z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M403.678,272c0,40.005-15.615,77.651-43.989,106.011C331.33,406.385,293.683,422,253.678,422
c-40.005,0-77.651-15.615-106.011-43.989c-28.374-28.359-43.989-66.006-43.989-106.011s15.615-77.651,43.989-106.011
C176.027,137.615,213.673,122,253.678,122c25.298,0,49.849,6.343,71.88,18.472L267.023,212h231.299L440.49,0l-57.393,70.13
C344.323,45.14,299.865,32,253.678,32c-64.116,0-124.395,24.961-169.702,70.298C38.639,147.605,13.678,207.884,13.678,272
s24.961,124.395,70.298,169.702C129.284,487.039,189.562,512,253.678,512s124.395-24.961,169.702-70.298
c45.337-45.308,70.298-105.586,70.298-169.702v-15h-90V272z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 262.474 262.474" style="enable-background:new 0 0 262.474 262.474;" xml:space="preserve">
<g id="XMLID_351_">
<path id="XMLID_352_" d="M217.327,210.541l-38.815-71.507l31.813-17.271c4.652-2.525,7.628-7.315,7.833-12.604
c0.204-5.289-2.395-10.294-6.837-13.17L66.756,2.408c-4.609-2.984-10.481-3.211-15.308-0.591c-4.826,2.62-7.835,7.667-7.844,13.159
l-0.277,172.206c-0.008,5.293,2.773,10.198,7.32,12.909c4.545,2.709,10.185,2.824,14.836,0.298l31.811-17.268l38.816,71.506
c2.718,5.007,7.873,7.847,13.196,7.847c2.417,0,4.869-0.586,7.143-1.82l54.849-29.774
C218.58,226.928,221.279,217.822,217.327,210.541z M155.321,227.132l-38.816-71.507c-1.898-3.496-5.107-6.095-8.922-7.225
c-1.395-0.414-2.831-0.618-4.261-0.618c-2.478,0-4.94,0.614-7.157,1.817l-22.797,12.376L73.56,42.55l100.255,64.898l-22.799,12.377
c-7.281,3.952-9.979,13.058-6.027,20.338l38.815,71.507L155.321,227.132z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 511.997 511.997" style="enable-background:new 0 0 511.997 511.997;" xml:space="preserve">
<g>
<g>
<rect x="137.809" y="471.577" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<rect x="283.463" y="471.577" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<path d="M60.963,471.581c-11.579,0-21-9.421-21-21.001v-19.959H0.044v19.959c0,33.592,27.327,60.92,60.919,60.92h19.959v-39.919
H60.963z"/>
</g>
</g>
<g>
<g>
<path d="M471.083,430.621v19.959c0,11.58-9.42,21.001-21,21.001h-19.959V511.5h19.959c33.592,0,60.919-27.328,60.919-60.92
v-19.959H471.083z"/>
</g>
</g>
<g>
<g>
<rect x="137.809" y="0.497" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<rect x="283.463" y="0.497" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<path d="M60.963,0.497c-33.592,0-60.92,27.328-60.92,60.92v19.959h39.919h0.001V61.417c0-11.58,9.42-21.001,21-21.001h19.959
V0.497H60.963z"/>
</g>
</g>
<g>
<g>
<path d="M450.083,0.497h-19.959v39.919h19.959c11.579,0,21,9.421,21,21.001v19.959h39.919V61.417
C511.002,27.825,483.675,0.497,450.083,0.497z"/>
</g>
</g>
<g>
<g>
<rect y="288.909" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect y="143.246" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect x="472.078" y="288.909" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect x="472.078" y="143.246" width="39.919" height="89.817"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 2 31 27" x="0px" y="0px">
<title>shapes</title>
<g>
<path d="M30.87,16.33l-7.5-13a1,1,0,0,0-1.73,0l-5,8.69a8.15,8.15,0,0,0-1.61,0V7a1,1,0,0,0-1-1H2A1,1,0,0,0,1,7V19a1,1,0,0,0,1,1H8a8,8,0,1,0,15.69-2.17H30a1,1,0,0,0,.87-1.5ZM3,18V8H13v4.58l0,0A8,8,0,0,0,8.28,18H3Zm13,8a6,6,0,0,1-6-6,4.62,4.62,0,0,1,.07-.86,6,6,0,0,1,4.22-4.89A5.92,5.92,0,0,1,16,14a5.29,5.29,0,0,1,1,.09A6,6,0,0,1,16,26Zm6.83-10.17h0a8,8,0,0,0-4.12-3.34l0,0L22.5,5.84l5.77,10Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 514 B

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M508.867,190.394l-111-143.568C394.996,43.113,390.52,41,386,41H126c-4.498,0-8.98,2.092-11.867,5.825l-111,143.568
c-4.509,5.832-4.112,14.076,0.937,19.447l241,256.432c2.655,2.824,6.568,4.728,10.93,4.728c4.3,0,8.221-1.845,10.93-4.728
l241-256.432C512.979,204.469,513.376,196.226,508.867,190.394z M241,418.137L34.694,198.62l93.584-121.043L241,172.957V418.137z
M256,146.351L166.949,71h178.102L256,146.351z M271,418.137v-245.18l112.722-95.38l93.585,121.043L271,418.137z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 867 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.527 13.164c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5h3.45c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967zM6.602 6.5H5.167a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607 0 .31.083.581.27.814z"/><path fill-rule="evenodd" d="M15 8.5H1v-1h14v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="977.7px" height="977.7px" viewBox="0 0 977.7 977.7" style="enable-background:new 0 0 977.7 977.7;" xml:space="preserve"
>
<g>
<path d="M770.7,930.6v-35.301c0-23.398-18-42.898-41.3-44.799c-17.9-1.5-35.8-3.1-53.7-5c-34.5-3.6-72.5-7.4-72.5-50.301L603,131.7
c136-2,210.5,76.7,250,193.2c6.3,18.7,23.8,31.3,43.5,31.3h36.2c24.9,0,45-20.1,45-45V47.1c0-24.9-20.1-45-45-45H488.9h-0.2H45
c-24.9,0-45,20.1-45,45v264.1c0,24.9,20.1,45,45,45h36.2c19.7,0,37.2-12.6,43.5-31.3c39.4-116.5,114-195.2,250-193.2l-0.3,663.5
c0,42.9-38,46.701-72.5,50.301c-17.9,1.9-35.8,3.5-53.7,5c-23.3,1.9-41.3,21.4-41.3,44.799V930.6c0,24.9,20.1,45,45,45h236.8h0.3
h236.7C750.5,975.6,770.7,955.401,770.7,930.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-type-underline" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57-1.709 0-2.687-1.08-2.687-2.57V3.136z"/><path fill-rule="evenodd" d="M12.5 15h-9v-1h9v1z"/></svg>

After

Width:  |  Height:  |  Size: 408 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.885 3.515c-4.617-4.618-12.056-4.676-16.756-.195l-2.129-2.258v7.938h7.484l-2.066-2.191c2.82-2.706 7.297-2.676 10.073.1 4.341 4.341 1.737 12.291-5.491 12.291v4.8c3.708 0 6.614-1.244 8.885-3.515 4.686-4.686 4.686-12.284 0-16.97z"/></svg>

After

Width:  |  Height:  |  Size: 330 B

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 461.312 461.312" style="enable-background:new 0 0 461.312 461.312;" xml:space="preserve">
<g>
<g>
<path d="M230.656,156.416c-40.96,0-74.24,33.28-74.24,74.24s33.28,74.24,74.24,74.24s74.24-33.28,74.24-74.24
S271.616,156.416,230.656,156.416z M225.024,208.64c-9.216,0-16.896,7.68-16.896,16.896h-24.576
c0.512-23.04,18.944-41.472,41.472-41.472V208.64z"/>
</g>
</g>
<g>
<g>
<path d="M455.936,215.296c-25.088-31.232-114.688-133.12-225.28-133.12S30.464,184.064,5.376,215.296
c-7.168,8.704-7.168,21.504,0,30.72c25.088,31.232,114.688,133.12,225.28,133.12s200.192-101.888,225.28-133.12
C463.104,237.312,463.104,224.512,455.936,215.296z M230.656,338.176c-59.392,0-107.52-48.128-107.52-107.52
s48.128-107.52,107.52-107.52s107.52,48.128,107.52,107.52S290.048,338.176,230.656,338.176z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

File diff suppressed because it is too large Load Diff
+102 -10
View File
@@ -1,12 +1,104 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html dir="ltr" lang="en-US">
<head> <head>
<meta charset="UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="x-ua-compatible" content="IE=edge" />
<title>AI Photo Edit</title> <title>miniPaint - image editor</title>
</head> <meta name="description" content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<body> <meta name="keywords" content="photo, image, picture, transparent, layers, free, edit, html5, canvas, javascript, online, photoshop, gimp, effects, sharpen, blur, magic eraser tool, clone tool, rotate, resize, photoshop online, online tools, tilt shift, sprites, keypoints" />
<div id="root"></div> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
<script type="module" src="/src/main.jsx"></script> <link rel="icon" sizes="192x192" href="images/favicon.png">
</body> <!-- <link rel="manifest" href="dist/manifest.json"> -->
<!-- Google -->
<meta itemprop="name" content="miniPaint" />
<meta itemprop="description" content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta itemprop="image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="miniPaint" />
<meta name="twitter:description" content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta name="twitter:image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<meta name="twitter:image:alt" content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<!-- Facebook, Pinterest -->
<meta property="og:title" content="miniPaint" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://viliusle.github.io/miniPaint/" />
<meta property="og:image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<meta property="og:description" content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta property="og:site_name" content="miniPaint" />
<script src="dist/bundle.js"></script>
</head>
<body>
<div class="wrapper">
<nav aria-label="Main Menu" class="main_menu" id="main_menu"></nav>
<div class="submenu">
<a class="logo" href="#">miniPaint</a>
<div class="block attributes" id="action_attributes"></div>
<button class="undo_button" id="undo_button" type="button">
<span class="sr_only">Undo</span>
</button>
</div>
<div class="sidebar_left" id="tools_container"></div>
<div class="middle_area" id="middle_area">
<canvas class="ruler_left" id="ruler_left"></canvas>
<canvas class="ruler_top" id="ruler_top"></canvas>
<div class="main_wrapper" id="main_wrapper">
<div class="canvas_wrapper" id="canvas_wrapper">
<div id="mouse"></div>
<div class="transparent-grid" id="canvas_minipaint_background"></div>
<canvas id="canvas_minipaint">
<div class="trn error">
Your browser does not support canvas or JavaScript is not enabled.
</div>
</canvas>
</div>
</div>
</div>
<div class="sidebar_right">
<div class="preview block">
<h2 class="trn toggle" data-target="toggle_preview">Preview</h2>
<div id="toggle_preview"></div>
</div>
<div class="colors block">
<h2 class="trn toggle" data-target="toggle_colors">Colors</h2>
<div class="content" id="toggle_colors"></div>
</div>
<div class="block" id="info_base">
<h2 class="trn toggle toggle-full" data-target="toggle_info">Information</h2>
<div class="content" id="toggle_info"></div>
</div>
<div class="details block" id="details_base">
<h2 class="trn toggle toggle-full" data-target="toggle_details">Layer details</h2>
<div class="content details-content" id="toggle_details"></div>
</div>
<div class="layers block">
<h2 class="trn">Layers</h2>
<div class="content" id="layers_base"></div>
</div>
</div>
</div>
<div class="mobile_menu">
<button class="left_mobile_menu" id="left_mobile_menu_button" type="button">
<span class="sr_only">Toggle Menu</span>
</button>
<button class="right_mobile_menu" id="mobile_menu_button" type="button">
<span class="sr_only">Toggle Menu</span>
</button>
</div>
<div class="hidden" id="tmp"></div>
<div id="popups"></div>
</body>
</html> </html>
+41
View File
@@ -0,0 +1,41 @@
{
"name": "miniPaint",
"short_name": "miniPaint",
"start_url": "/",
"display": "standalone",
"orientation": "landscape",
"background_color": "#666d6f",
"description": "miniPaint is free online image editor using HTML5.",
"icons": [
{
"src": "images/manifest/48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "images/manifest/72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/manifest/96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/manifest/144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/manifest/168x168.png",
"sizes": "168x168",
"type": "image/png"
},
{
"src": "images/manifest/192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
+10750
View File
File diff suppressed because it is too large Load Diff
+45 -17
View File
@@ -1,23 +1,51 @@
{ {
"name": "ai-photo-edit-frontend", "name": "miniPaint",
"version": "1.0.0", "version": "4.14.2",
"type": "module", "author": "Vilius L.",
"description": "Online graphics editing tool lets create, edit images using HTML5 technologies.",
"keywords": [
"canvas",
"drawing",
"paint",
"layers",
"effects"
],
"scripts": { "scripts": {
"dev": "vite", "server": "webpack serve --mode development --env development --open",
"build": "vite build", "dev": "webpack --mode development",
"preview": "vite preview", "build": "webpack --mode production"
"lint": "eslint ." },
"repository": {
"type": "git",
"url": "https://github.com/viliusle/miniPaint"
},
"homepage": "https://github.com/viliusle/miniPaint",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"babel-loader": "^8.2.2",
"css-loader": "^5.2.6",
"source-map-loader": "^3.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.76.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^4.3.1"
}, },
"dependencies": { "dependencies": {
"react": "^18.2.0", "@babel/runtime": "^7.14.5",
"react-dom": "^18.2.0", "alertifyjs": "^1.13.1",
"fabric": "^5.3.0", "blueimp-canvas-to-blob": "^3.28.0",
"axios": "^1.6.5" "exif-js": "^2.3.0",
}, "file-saver": "^2.0.5",
"devDependencies": { "fuzzysort": "^1.1.4",
"@types/react": "^18.2.48", "gif.js.optimized": "^1.0.1",
"@types/react-dom": "^18.2.18", "hermite-resize": "git+https://github.com/viliusle/Hermite-resize.git",
"@vitejs/plugin-react": "^4.2.1", "jquery": "^3.5.1",
"vite": "^5.0.12" "pica": "^7.0.0",
"semver-compare": "^1.0.0",
"uuid": "^8.3.2",
"webfontloader": "^1.6.28"
} }
} }
+68
View File
@@ -0,0 +1,68 @@
//IMPORTANT - this file is not used !!!
// use a cacheName for cache versioning
var cacheName = 'v1:static';
// during the install phase you usually want to cache static assets
self.addEventListener('install', function(e) {
// once the SW is installed, go ahead and fetch the resources to make this work offline
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll([
'./',
'./dist/bundle.js',
'./images/favicon.png',
'./images/logo.svg',
'./images/logo-colors.png',
'./images/icons/animation.svg',
'./images/icons/blur.svg',
'./images/icons/bold.svg',
'./images/icons/brush.svg',
'./images/icons/bulge_pinch.svg',
'./images/icons/clone.svg',
'./images/icons/crop.svg',
'./images/icons/delete.svg',
'./images/icons/desaturate.svg',
'./images/icons/erase.svg',
'./images/icons/external.png',
'./images/icons/fill.svg',
'./images/icons/gradient.png',
'./images/icons/grid.png',
'./images/icons/italic.svg',
'./images/icons/magic_erase.svg',
'./images/icons/media.svg',
'./images/icons/menu.svg',
'./images/icons/pencil.svg',
'./images/icons/pick_color.svg',
'./images/icons/refresh.svg',
'./images/icons/select.svg',
'./images/icons/selection.svg',
'./images/icons/shape.svg',
'./images/icons/sharpen.svg',
'./images/icons/strikethrough.svg',
'./images/icons/text.svg',
'./images/icons/underline.svg',
'./images/icons/view.svg'
]).then(function() {
self.skipWaiting();
});
})
);
});
// when the browser fetches a url
self.addEventListener('fetch', function(event) {
// either respond with the cached object or go ahead and fetch the actual url
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
// retrieve from cache
return response;
}
// fetch as normal
return fetch(event.request);
})
);
});
-652
View File
@@ -1,652 +0,0 @@
/* GIMP-like layout */
* {
box-sizing: border-box;
}
html, body, #root {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.app {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #1a1a1a;
color: #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Header / Menu bar */
.menu-bar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #2d2d2d;
border-bottom: 1px solid #404040;
padding: 4px 12px;
height: 36px;
flex-shrink: 0;
}
.menu-bar h1 {
font-size: 14px;
font-weight: 600;
margin: 0;
color: #fff;
}
.menu-actions {
display: flex;
gap: 8px;
}
.menu-btn {
background: #404040;
border: none;
color: #e0e0e0;
padding: 4px 12px;
font-size: 12px;
border-radius: 3px;
cursor: pointer;
}
.menu-btn:hover {
background: #505050;
}
.menu-btn.primary {
background: #0066cc;
}
.menu-btn.primary:hover {
background: #0077ee;
}
/* Main workspace */
.workspace {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* Left toolbar - GIMP style vertical tools */
.toolbar {
width: 48px;
background-color: #2d2d2d;
border-right: 1px solid #404040;
display: flex;
flex-direction: column;
padding: 4px;
gap: 2px;
flex-shrink: 0;
}
.tool-btn {
width: 40px;
height: 40px;
background: transparent;
border: 2px solid transparent;
border-radius: 4px;
color: #b0b0b0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.15s;
}
.tool-btn:hover {
background: #404040;
color: #fff;
}
.tool-btn.active {
background: #0066cc;
border-color: #0088ff;
color: #fff;
}
.tool-btn[title]:hover::after {
content: attr(title);
position: absolute;
left: 52px;
background: #000;
color: #fff;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
white-space: nowrap;
z-index: 100;
}
.tool-divider {
height: 1px;
background: #404040;
margin: 4px 0;
}
/* Canvas area - main editing space */
.canvas-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: #1a1a1a;
position: relative;
}
.canvas-wrapper {
flex: 1;
position: relative;
overflow: hidden;
background:
linear-gradient(45deg, #252525 25%, transparent 25%),
linear-gradient(-45deg, #252525 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #252525 75%),
linear-gradient(-45deg, transparent 75%, #252525 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-color: #1e1e1e;
}
/* Canvas status bar */
.canvas-status {
height: 24px;
background: #2d2d2d;
border-top: 1px solid #404040;
display: flex;
align-items: center;
padding: 0 12px;
font-size: 11px;
color: #888;
gap: 20px;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 8px;
}
.zoom-btn {
background: #404040;
border: none;
color: #e0e0e0;
width: 24px;
height: 20px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.zoom-btn:hover {
background: #505050;
}
.zoom-level {
min-width: 50px;
text-align: center;
}
/* Right sidebar */
.sidebar {
width: 280px;
background-color: #2d2d2d;
border-left: 1px solid #404040;
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-panel {
border-bottom: 1px solid #404040;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #353535;
cursor: pointer;
user-select: none;
}
.panel-header h3 {
font-size: 12px;
font-weight: 600;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.panel-toggle {
font-size: 10px;
color: #888;
}
.panel-content {
padding: 12px;
}
.panel-content.collapsed {
display: none;
}
/* Form controls */
.control-group {
margin-bottom: 12px;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
display: block;
font-size: 11px;
color: #888;
margin-bottom: 4px;
text-transform: uppercase;
}
.control-row {
display: flex;
gap: 4px;
}
.mode-btn {
flex: 1;
padding: 6px 8px;
background: #404040;
border: 1px solid #505050;
color: #e0e0e0;
font-size: 11px;
border-radius: 3px;
cursor: pointer;
}
.mode-btn:hover {
background: #505050;
}
.mode-btn.active {
background: #0066cc;
border-color: #0088ff;
}
input[type="range"] {
width: 100%;
height: 4px;
background: #404040;
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #0088ff;
border-radius: 50%;
cursor: pointer;
}
textarea {
width: 100%;
background: #1a1a1a;
border: 1px solid #404040;
border-radius: 3px;
color: #e0e0e0;
padding: 8px;
font-size: 12px;
resize: vertical;
min-height: 60px;
}
textarea:focus {
border-color: #0088ff;
outline: none;
}
.action-btn {
width: 100%;
padding: 10px;
background: #0066cc;
border: none;
color: white;
font-size: 12px;
font-weight: 600;
border-radius: 3px;
cursor: pointer;
text-transform: uppercase;
}
.action-btn:hover:not(:disabled) {
background: #0077ee;
}
.action-btn:disabled {
background: #404040;
color: #666;
cursor: not-allowed;
}
.action-btn.secondary {
background: #404040;
}
.action-btn.secondary:hover:not(:disabled) {
background: #505050;
}
.action-btn.danger {
background: #cc3333;
}
.action-btn.danger:hover:not(:disabled) {
background: #dd4444;
}
/* Error banner */
.error-banner {
background-color: #cc3333;
color: white;
padding: 8px 12px;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-banner button {
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
padding: 0;
}
/* Project setup modal */
.project-setup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.project-setup {
background: #2d2d2d;
border: 1px solid #404040;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90vw;
}
.project-setup h2 {
font-size: 18px;
margin: 0 0 20px 0;
text-align: center;
}
.setup-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 12px;
color: #888;
text-transform: uppercase;
}
.form-group input[type="text"],
.form-group input[type="file"] {
background: #1a1a1a;
border: 1px solid #404040;
border-radius: 3px;
color: #e0e0e0;
padding: 10px;
font-size: 14px;
}
.form-group input[type="text"]:focus {
border-color: #0088ff;
outline: none;
}
.file-name {
font-size: 11px;
color: #0088ff;
}
.create-project-btn {
background: #0066cc;
border: none;
color: white;
padding: 12px;
font-size: 14px;
font-weight: 600;
border-radius: 3px;
cursor: pointer;
margin-top: 8px;
}
.create-project-btn:hover:not(:disabled) {
background: #0077ee;
}
.create-project-btn:disabled {
background: #404040;
color: #666;
}
/* Layer list */
.layer-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.layer-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: #353535;
border-radius: 3px;
cursor: pointer;
}
.layer-item:hover {
background: #404040;
}
.layer-item.active {
background: #0066cc;
}
.layer-visibility {
width: 16px;
height: 16px;
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0;
font-size: 12px;
}
.layer-visibility.visible {
color: #0088ff;
}
.layer-name {
flex: 1;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* History list */
.history-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 200px;
overflow-y: auto;
}
.history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background: #353535;
border-radius: 3px;
font-size: 11px;
}
.history-item:hover {
background: #404040;
}
.history-item.current {
background: #0066cc;
}
.history-item-actions {
display: flex;
gap: 4px;
}
.history-btn {
background: #505050;
border: none;
color: #e0e0e0;
padding: 2px 8px;
font-size: 10px;
border-radius: 2px;
cursor: pointer;
}
.history-btn:hover {
background: #606060;
}
/* Eye catalog */
.eye-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.eye-item {
aspect-ratio: 1;
background: #353535;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
}
.eye-item:hover {
border-color: #0088ff;
}
.eye-item.selected {
border-color: #0088ff;
}
.eye-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Processing overlay */
.processing-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.processing-spinner {
width: 40px;
height: 40px;
border: 3px solid #404040;
border-top-color: #0088ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Slider with value */
.slider-row {
display: flex;
align-items: center;
gap: 8px;
}
.slider-row input[type="range"] {
flex: 1;
}
.slider-value {
font-size: 11px;
color: #0088ff;
min-width: 35px;
text-align: right;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #404040;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #505050;
}
-698
View File
@@ -1,698 +0,0 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import ImageCanvas from './components/ImageCanvas';
import { projectsApi, editsApi, toolsApi } from './utils/api';
import './App.css';
// Tool definitions
const TOOLS = {
move: { icon: '✥', name: 'Move', shortcut: 'V' },
select: { icon: '▢', name: 'Rectangle Select', shortcut: 'R' },
ellipse: { icon: '○', name: 'Ellipse Select', shortcut: 'E' },
lasso: { icon: '✎', name: 'Free Select (Lasso)', shortcut: 'F' },
magic: { icon: '✨', name: 'Smart Select (SAM)', shortcut: 'W' },
colorPick: { icon: '◉', name: 'Color Select', shortcut: 'U' },
brush: { icon: '🖌', name: 'Brush', shortcut: 'B' },
bucket: { icon: '◧', name: 'Bucket Fill', shortcut: 'G' },
eraser: { icon: '◫', name: 'Eraser', shortcut: 'Shift+E' },
eyedropper: { icon: '💧', name: 'Color Picker', shortcut: 'O' },
zoom: { icon: '🔍', name: 'Zoom', shortcut: 'Z' },
pan: { icon: '✋', name: 'Pan', shortcut: 'H' },
};
function App() {
// Project state
const [project, setProject] = useState(null);
const [imageFile, setImageFile] = useState(null);
const [currentImageUrl, setCurrentImageUrl] = useState(null);
const [showProjectSetup, setShowProjectSetup] = useState(true);
const [projectName, setProjectName] = useState('');
// Tool state
const [activeTool, setActiveTool] = useState('select');
const [selection, setSelection] = useState(null);
// Edit state
const [prompt, setPrompt] = useState('');
const [feather, setFeather] = useState(5);
const [mode, setMode] = useState('A');
const [edits, setEdits] = useState([]);
const [currentEditIndex, setCurrentEditIndex] = useState(-1);
const editsRef = useRef([]);
// Layer state
const [layers, setLayers] = useState([]);
// UI state
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
const [zoom, setZoom] = useState(100);
const [collapsedPanels, setCollapsedPanels] = useState({});
const [eyes, setEyes] = useState([]);
const [selectedEye, setSelectedEye] = useState(null);
// Canvas ref for tool interactions
const canvasRef = useRef(null);
// Map tool to selection mode
const getSelectionMode = () => {
switch (activeTool) {
case 'select': return 'rectangle';
case 'ellipse': return 'ellipse';
case 'lasso': return 'lasso';
case 'magic': return 'smart';
case 'colorPick': return 'color';
default: return null;
}
};
// Load eyes catalog
useEffect(() => {
const loadEyes = async () => {
try {
const patches = await fetch('/patches/?category=eyes').then(r => r.json()).catch(() => []);
setEyes(patches);
} catch (err) {
console.error('Failed to load eyes:', err);
}
};
if (project) loadEyes();
}, [project]);
// Create project
const handleCreateProject = async () => {
if (!imageFile) {
setError('Please select an image');
return;
}
try {
setError(null);
setIsProcessing(true);
const defaultName = projectName.trim() ||
imageFile.name.replace(/\.[^/.]+$/, '') ||
`Project ${Date.now()}`;
const newProject = await projectsApi.create(defaultName);
setProject(newProject);
await projectsApi.uploadImage(newProject.id, imageFile);
setCurrentImageUrl(projectsApi.getCurrentImageUrl(newProject.id));
setShowProjectSetup(false);
await loadEdits(newProject.id);
} catch (err) {
setError(`Failed to create project: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
// Load edits
const loadEdits = async (projectId) => {
try {
const projectEdits = await projectsApi.getEdits(projectId);
setEdits(projectEdits);
editsRef.current = projectEdits;
const completedEdits = projectEdits.filter(e => e.status === 'completed');
setCurrentEditIndex(completedEdits.length - 1);
} catch (err) {
console.error('Failed to load edits:', err);
}
};
// Poll edit status
const pollEditStatus = async (editId) => {
const maxAttempts = 120;
let attempts = 0;
const poll = async () => {
try {
const edit = await editsApi.get(editId);
if (edit.status === 'completed') {
await loadEdits(project.id);
setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id) + `?t=${Date.now()}`);
setIsProcessing(false);
setSelection(null);
return;
} else if (edit.status === 'failed') {
setError(`Edit failed: ${edit.error_message}`);
setIsProcessing(false);
return;
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(poll, 1000);
} else {
setError('Edit timeout');
setIsProcessing(false);
}
} catch (err) {
setError(`Status check failed: ${err.message}`);
setIsProcessing(false);
}
};
poll();
};
// Handle AI fix
const handleFix = async () => {
if (!selection || !prompt.trim() || !project) {
setError('Make a selection and enter a prompt');
return;
}
try {
setError(null);
setIsProcessing(true);
const editRequest = {
prompt: prompt.trim(),
mode: mode,
selection_type: selection.type,
bbox: selection.bbox,
feather_px: feather,
selection_data: selection.selectionData,
};
const edit = await editsApi.create(project.id, editRequest);
pollEditStatus(edit.id);
} catch (err) {
setError(`Edit failed: ${err.message}`);
setIsProcessing(false);
}
};
// Undo/Redo
const handleRevert = useCallback(async (editId) => {
if (!project) return;
try {
setIsProcessing(true);
await editsApi.revert(project.id, editId);
setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id) + `?t=${Date.now()}`);
await loadEdits(project.id);
} catch (err) {
setError(`Revert failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
}, [project]);
const handleReset = useCallback(async () => {
if (!project) return;
try {
setIsProcessing(true);
await editsApi.reset(project.id);
setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id) + `?t=${Date.now()}`);
await loadEdits(project.id);
} catch (err) {
setError(`Reset failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
}, [project]);
// Download
const handleDownload = async () => {
if (!currentImageUrl) return;
try {
const response = await fetch(`${currentImageUrl}&download=1`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${project?.name || 'image'}-${Date.now()}.png`;
link.click();
window.URL.revokeObjectURL(url);
} catch (err) {
setError(`Download failed: ${err.message}`);
}
};
// Smart Select (SAM)
const handleSmartSelect = async (x, y) => {
if (!project) return;
try {
setIsProcessing(true);
const maskBlob = await toolsApi.smartSelect(project.id, x, y);
// TODO: Display mask on canvas
console.log('Smart select mask:', maskBlob);
} catch (err) {
setError(`Smart select failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
// Remove background
const handleRemoveBackground = async () => {
if (!project) return;
try {
setIsProcessing(true);
await toolsApi.removeBackgroundToLayer(project.id);
setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id) + `?t=${Date.now()}`);
} catch (err) {
setError(`Background removal failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
// Apply eye
const handleApplyEye = async () => {
if (!project || !selection || !selectedEye) {
setError('Select an area and an eye to apply');
return;
}
try {
setIsProcessing(true);
const formData = new FormData();
formData.append('project_id', project.id);
formData.append('patch_id', selectedEye.id);
formData.append('bbox', JSON.stringify(selection.bbox));
formData.append('feather_px', feather);
await fetch('/patches/apply', {
method: 'POST',
body: formData,
});
setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id) + `?t=${Date.now()}`);
await loadEdits(project.id);
} catch (err) {
setError(`Apply eye failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Tool shortcuts
if (!e.ctrlKey && !e.metaKey) {
switch (e.key.toLowerCase()) {
case 'v': setActiveTool('move'); break;
case 'r': setActiveTool('select'); break;
case 'e': setActiveTool('ellipse'); break;
case 'f': setActiveTool('lasso'); break;
case 'w': setActiveTool('magic'); break;
case 'b': setActiveTool('brush'); break;
case 'g': setActiveTool('bucket'); break;
case 'z': setActiveTool('zoom'); break;
case 'h': setActiveTool('pan'); break;
case 'delete':
case 'backspace':
if (selection) {
// TODO: Delete selected area
}
break;
}
}
// Ctrl shortcuts
if (e.ctrlKey || e.metaKey) {
switch (e.key.toLowerCase()) {
case 'z':
e.preventDefault();
if (e.shiftKey) {
// Redo
const completed = editsRef.current.filter(ed => ed.status === 'completed');
if (currentEditIndex < completed.length - 1) {
handleRevert(completed[currentEditIndex + 1].id);
setCurrentEditIndex(i => i + 1);
}
} else {
// Undo
if (currentEditIndex >= 0) {
if (currentEditIndex === 0) {
handleReset();
} else {
const completed = editsRef.current.filter(ed => ed.status === 'completed');
handleRevert(completed[currentEditIndex - 1].id);
}
setCurrentEditIndex(i => i - 1);
}
}
break;
case 's':
e.preventDefault();
handleDownload();
break;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selection, currentEditIndex, handleRevert, handleReset]);
// Zoom controls
const handleZoomIn = () => setZoom(z => Math.min(z + 25, 400));
const handleZoomOut = () => setZoom(z => Math.max(z - 25, 25));
const handleZoomReset = () => setZoom(100);
// Panel toggle
const togglePanel = (panel) => {
setCollapsedPanels(prev => ({ ...prev, [panel]: !prev[panel] }));
};
// Project setup overlay
if (showProjectSetup) {
return (
<div className="app">
<div className="project-setup-overlay">
<div className="project-setup">
<h2>Open Image</h2>
<div className="setup-form">
<div className="form-group">
<label>Select Image</label>
<input
type="file"
accept="image/*"
onChange={(e) => setImageFile(e.target.files[0])}
disabled={isProcessing}
/>
{imageFile && <p className="file-name">{imageFile.name}</p>}
</div>
<div className="form-group">
<label>Project Name (optional)</label>
<input
type="text"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
placeholder="Auto-generated from filename"
disabled={isProcessing}
/>
</div>
<button
className="create-project-btn"
onClick={handleCreateProject}
disabled={isProcessing || !imageFile}
>
{isProcessing ? 'Opening...' : 'Open'}
</button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="app">
{/* Menu Bar */}
<div className="menu-bar">
<h1>AI Photo Edit</h1>
<div className="menu-actions">
<button className="menu-btn" onClick={() => setShowProjectSetup(true)}>New</button>
<button className="menu-btn" onClick={handleDownload}>Save</button>
<button className="menu-btn" onClick={handleReset}>Reset</button>
</div>
</div>
{/* Error Banner */}
{error && (
<div className="error-banner">
{error}
<button onClick={() => setError(null)}></button>
</div>
)}
{/* Main Workspace */}
<div className="workspace">
{/* Left Toolbar */}
<div className="toolbar">
<button
className={`tool-btn ${activeTool === 'move' ? 'active' : ''}`}
onClick={() => setActiveTool('move')}
title="Move (V)"
></button>
<div className="tool-divider" />
<button
className={`tool-btn ${activeTool === 'select' ? 'active' : ''}`}
onClick={() => setActiveTool('select')}
title="Rectangle Select (R)"
></button>
<button
className={`tool-btn ${activeTool === 'ellipse' ? 'active' : ''}`}
onClick={() => setActiveTool('ellipse')}
title="Ellipse Select (E)"
></button>
<button
className={`tool-btn ${activeTool === 'lasso' ? 'active' : ''}`}
onClick={() => setActiveTool('lasso')}
title="Free Select (F)"
></button>
<button
className={`tool-btn ${activeTool === 'magic' ? 'active' : ''}`}
onClick={() => setActiveTool('magic')}
title="Smart Select - SAM (W)"
></button>
<button
className={`tool-btn ${activeTool === 'colorPick' ? 'active' : ''}`}
onClick={() => setActiveTool('colorPick')}
title="Color Select (U)"
></button>
<div className="tool-divider" />
<button
className={`tool-btn ${activeTool === 'brush' ? 'active' : ''}`}
onClick={() => setActiveTool('brush')}
title="Brush (B)"
>🖌</button>
<button
className={`tool-btn ${activeTool === 'bucket' ? 'active' : ''}`}
onClick={() => setActiveTool('bucket')}
title="Bucket Fill (G)"
></button>
<button
className={`tool-btn ${activeTool === 'eraser' ? 'active' : ''}`}
onClick={() => setActiveTool('eraser')}
title="Eraser (Shift+E)"
></button>
<div className="tool-divider" />
<button
className={`tool-btn ${activeTool === 'eyedropper' ? 'active' : ''}`}
onClick={() => setActiveTool('eyedropper')}
title="Color Picker (O)"
>💧</button>
<button
className={`tool-btn ${activeTool === 'zoom' ? 'active' : ''}`}
onClick={() => setActiveTool('zoom')}
title="Zoom (Z)"
>🔍</button>
<button
className={`tool-btn ${activeTool === 'pan' ? 'active' : ''}`}
onClick={() => setActiveTool('pan')}
title="Pan (H)"
></button>
</div>
{/* Canvas Area */}
<div className="canvas-area">
<div className="canvas-wrapper">
<ImageCanvas
ref={canvasRef}
imageUrl={currentImageUrl}
onSelectionChange={setSelection}
selectionMode={getSelectionMode()}
activeTool={activeTool}
zoom={zoom}
onSmartSelect={handleSmartSelect}
isProcessing={isProcessing}
/>
{isProcessing && (
<div className="processing-overlay">
<div className="processing-spinner" />
</div>
)}
</div>
<div className="canvas-status">
<div className="zoom-controls">
<button className="zoom-btn" onClick={handleZoomOut}></button>
<span className="zoom-level">{zoom}%</span>
<button className="zoom-btn" onClick={handleZoomIn}>+</button>
<button className="zoom-btn" onClick={handleZoomReset}></button>
</div>
<span>Selection: {selection ? `${selection.bbox?.width || 0}×${selection.bbox?.height || 0}` : 'None'}</span>
<span>Tool: {TOOLS[activeTool]?.name || activeTool}</span>
</div>
</div>
{/* Right Sidebar */}
<div className="sidebar">
{/* Tool Options Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('toolOptions')}>
<h3>Tool Options</h3>
<span className="panel-toggle">{collapsedPanels.toolOptions ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.toolOptions ? 'collapsed' : ''}`}>
<div className="control-group">
<label className="control-label">Feather</label>
<div className="slider-row">
<input
type="range"
min="0"
max="50"
value={feather}
onChange={(e) => setFeather(parseInt(e.target.value))}
/>
<span className="slider-value">{feather}px</span>
</div>
</div>
</div>
</div>
{/* AI Edit Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('aiEdit')}>
<h3>AI Edit</h3>
<span className="panel-toggle">{collapsedPanels.aiEdit ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.aiEdit ? 'collapsed' : ''}`}>
<div className="control-group">
<label className="control-label">Mode</label>
<div className="control-row">
<button
className={`mode-btn ${mode === 'A' ? 'active' : ''}`}
onClick={() => setMode('A')}
>Patch Only</button>
<button
className={`mode-btn ${mode === 'B' ? 'active' : ''}`}
onClick={() => setMode('B')}
>With Context</button>
</div>
</div>
<div className="control-group">
<label className="control-label">Prompt</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe what to fix or change..."
/>
</div>
<button
className="action-btn"
onClick={handleFix}
disabled={isProcessing || !selection || !prompt.trim()}
>
{isProcessing ? 'Processing...' : 'Apply AI Edit'}
</button>
</div>
</div>
{/* Quick Actions Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('actions')}>
<h3>Quick Actions</h3>
<span className="panel-toggle">{collapsedPanels.actions ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.actions ? 'collapsed' : ''}`}>
<button
className="action-btn secondary"
onClick={handleRemoveBackground}
disabled={isProcessing}
>Remove Background</button>
</div>
</div>
{/* Eyes Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('eyes')}>
<h3>Eye Catalog</h3>
<span className="panel-toggle">{collapsedPanels.eyes ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.eyes ? 'collapsed' : ''}`}>
{eyes.length > 0 ? (
<>
<div className="eye-grid">
{eyes.map(eye => (
<div
key={eye.id}
className={`eye-item ${selectedEye?.id === eye.id ? 'selected' : ''}`}
onClick={() => setSelectedEye(eye)}
>
<img src={`/patches/${eye.id}/image?thumbnail=true`} alt={eye.name} />
</div>
))}
</div>
<button
className="action-btn"
onClick={handleApplyEye}
disabled={isProcessing || !selection || !selectedEye}
style={{ marginTop: '12px' }}
>Apply Eye to Selection</button>
</>
) : (
<p style={{ fontSize: '12px', color: '#888' }}>No eyes in catalog</p>
)}
</div>
</div>
{/* Layers Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('layers')}>
<h3>Layers</h3>
<span className="panel-toggle">{collapsedPanels.layers ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.layers ? 'collapsed' : ''}`}>
<div className="layer-list">
<div className="layer-item active">
<button className="layer-visibility visible">👁</button>
<span className="layer-name">Background</span>
</div>
{layers.map((layer) => (
<div key={layer.id} className="layer-item">
<button className="layer-visibility visible">👁</button>
<span className="layer-name">{layer.name}</span>
</div>
))}
</div>
</div>
</div>
{/* History Panel */}
<div className="sidebar-panel">
<div className="panel-header" onClick={() => togglePanel('history')}>
<h3>History</h3>
<span className="panel-toggle">{collapsedPanels.history ? '▶' : '▼'}</span>
</div>
<div className={`panel-content ${collapsedPanels.history ? 'collapsed' : ''}`}>
<div className="history-list">
<div
className={`history-item ${currentEditIndex === -1 ? 'current' : ''}`}
onClick={handleReset}
>
Original
</div>
{edits.filter(e => e.status === 'completed').map((edit, i) => (
<div
key={edit.id}
className={`history-item ${currentEditIndex === i ? 'current' : ''}`}
onClick={() => handleRevert(edit.id)}
>
{edit.prompt?.substring(0, 30) || `Edit ${i + 1}`}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;
-107
View File
@@ -1,107 +0,0 @@
.advanced-tools {
background-color: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
padding: 16px;
}
.advanced-tools h3 {
margin: 0 0 16px 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.tool-group {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #333;
}
.tool-group:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.tool-group h4 {
font-size: 12px;
color: #888;
margin: 0 0 8px 0;
text-transform: uppercase;
}
.tool-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tool-btn {
flex: 1;
min-width: 100px;
background-color: #3a3a3a;
color: #ffffff;
padding: 10px 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid #555;
transition: all 0.2s;
}
.tool-btn:hover:not(:disabled) {
background-color: #4a4a4a;
border-color: #666;
}
.tool-btn.active {
background-color: #0066ff;
border-color: #0066ff;
color: white;
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.color-tolerance {
margin-top: 10px;
padding: 10px;
background-color: #333;
border-radius: 4px;
}
.color-tolerance label {
display: block;
font-size: 12px;
color: #888;
margin-bottom: 6px;
}
.color-tolerance input[type="range"] {
width: 100%;
}
.tool-hint {
font-size: 11px;
color: #00ff00;
margin-top: 8px;
padding: 8px;
background-color: rgba(0, 255, 0, 0.1);
border-radius: 4px;
text-align: center;
}
.cancel-mode-btn {
width: 100%;
margin-top: 12px;
background-color: #ff4444;
color: white;
padding: 8px;
font-size: 12px;
}
.cancel-mode-btn:hover {
background-color: #cc0000;
}
-183
View File
@@ -1,183 +0,0 @@
import React, { useState } from 'react';
import { toolsApi } from '../utils/api';
import './AdvancedTools.css';
const AdvancedTools = ({
projectId,
selection,
onLayerCreated,
onMaskGenerated,
onImageUpdate,
isProcessing,
setIsProcessing,
setError,
activeToolMode,
setActiveToolMode,
}) => {
const [colorTolerance, setColorTolerance] = useState(30);
const handleRemoveBackground = async () => {
if (!projectId) return;
try {
setIsProcessing(true);
setError(null);
const result = await toolsApi.removeBackgroundToLayer(projectId);
onLayerCreated(result.layer);
} catch (err) {
setError(`Background removal failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleSmartSelect = () => {
setActiveToolMode(activeToolMode === 'smart-select' ? null : 'smart-select');
};
const handleColorSelect = () => {
setActiveToolMode(activeToolMode === 'color-select' ? null : 'color-select');
};
const handleObjectRemove = () => {
setActiveToolMode(activeToolMode === 'object-remove' ? null : 'object-remove');
};
// Called when user clicks on canvas in smart-select mode
const onCanvasClick = async (x, y) => {
if (!projectId || !activeToolMode) return;
if (activeToolMode === 'smart-select') {
try {
setIsProcessing(true);
const maskBlob = await toolsApi.smartSelect(projectId, x, y);
onMaskGenerated(maskBlob, 'smart-select');
} catch (err) {
setError(`Smart select failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
}
};
// Called when user picks a color for color selection
const onColorPicked = async (r, g, b) => {
if (!projectId) return;
try {
setIsProcessing(true);
const maskBlob = await toolsApi.colorSelect(projectId, r, g, b, colorTolerance);
onMaskGenerated(maskBlob, 'color-select');
} catch (err) {
setError(`Color select failed: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleExtractObject = async () => {
if (!projectId || !selection) {
setError('Make a selection first');
return;
}
// For this we need the mask from the current selection
// This would be generated from the selection shape
setError('Extract requires a mask - use Smart Select or Color Select first');
};
return (
<div className="advanced-tools">
<h3>Advanced Tools</h3>
<div className="tool-group">
<h4>Background</h4>
<button
className="tool-btn"
onClick={handleRemoveBackground}
disabled={isProcessing || !projectId}
title="Remove background and create new layer"
>
Remove Background
</button>
</div>
<div className="tool-group">
<h4>Selection Tools</h4>
<div className="tool-buttons">
<button
className={`tool-btn ${activeToolMode === 'smart-select' ? 'active' : ''}`}
onClick={handleSmartSelect}
disabled={isProcessing || !projectId}
title="Click on object to select it"
>
Smart Select
</button>
<button
className={`tool-btn ${activeToolMode === 'color-select' ? 'active' : ''}`}
onClick={handleColorSelect}
disabled={isProcessing || !projectId}
title="Select all similar colors"
>
Color Select
</button>
</div>
{activeToolMode === 'color-select' && (
<div className="color-tolerance">
<label>Tolerance: {colorTolerance}</label>
<input
type="range"
min="1"
max="100"
value={colorTolerance}
onChange={(e) => setColorTolerance(parseInt(e.target.value))}
/>
</div>
)}
{activeToolMode && (
<p className="tool-hint">
{activeToolMode === 'smart-select'
? 'Click on an object to select it'
: 'Click on a color to select all similar pixels'}
</p>
)}
</div>
<div className="tool-group">
<h4>Object Tools</h4>
<button
className={`tool-btn ${activeToolMode === 'object-remove' ? 'active' : ''}`}
onClick={handleObjectRemove}
disabled={isProcessing || !projectId}
title="Remove selected object (uses AI)"
>
Object Remove
</button>
<button
className="tool-btn"
onClick={handleExtractObject}
disabled={isProcessing || !projectId || !selection}
title="Extract selected area to new layer"
>
Extract to Layer
</button>
</div>
{activeToolMode && (
<button
className="cancel-mode-btn"
onClick={() => setActiveToolMode(null)}
>
Cancel Tool
</button>
)}
</div>
);
};
// Export the click handler for parent component to use
AdvancedTools.handleCanvasClick = null;
export default AdvancedTools;
-134
View File
@@ -1,134 +0,0 @@
.controls {
padding: 20px;
background-color: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
}
.control-section {
margin-bottom: 24px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #ffffff;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.button-group button {
flex: 1;
min-width: 100px;
background-color: #3a3a3a;
color: #ffffff;
padding: 10px 16px;
font-weight: 500;
}
.button-group button:hover:not(:disabled) {
background-color: #4a4a4a;
}
.button-group button.active {
background-color: #00aa00;
color: #ffffff;
}
.button-group button.active:hover:not(:disabled) {
background-color: #00cc00;
}
.mode-hint {
margin-top: 8px;
font-size: 12px;
color: #888;
font-style: italic;
}
.slider-group {
display: flex;
align-items: center;
gap: 12px;
}
.slider-group input[type="range"] {
flex: 1;
}
.slider-value {
min-width: 50px;
text-align: right;
font-weight: 600;
color: #00ff00;
}
.hint {
margin-top: 6px;
font-size: 12px;
color: #666;
}
.control-section textarea {
width: 100%;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 10px;
}
.fix-btn {
background-color: #0066ff;
color: white;
padding: 14px 20px;
font-size: 16px;
font-weight: 600;
}
.fix-btn:hover:not(:disabled) {
background-color: #0055dd;
}
.fix-btn:disabled {
background-color: #333;
color: #666;
}
.clear-btn {
background-color: #ff4444;
color: white;
padding: 10px 16px;
}
.clear-btn:hover:not(:disabled) {
background-color: #cc0000;
}
.download-btn {
width: 100%;
background-color: #00aa00;
color: white;
padding: 14px 20px;
font-size: 16px;
font-weight: 600;
}
.download-btn:hover:not(:disabled) {
background-color: #00cc00;
}
-124
View File
@@ -1,124 +0,0 @@
import React from 'react';
import './Controls.css';
const Controls = ({
selectionMode,
onSelectionModeChange,
mode,
onModeChange,
feather,
onFeatherChange,
prompt,
onPromptChange,
onFix,
onDownload,
isProcessing,
hasSelection,
}) => {
return (
<div className="controls">
<div className="control-section">
<h3>Selection Tool</h3>
<div className="button-group">
<button
className={selectionMode === 'rectangle' ? 'active' : ''}
onClick={() => onSelectionModeChange('rectangle')}
disabled={isProcessing}
>
Rectangle
</button>
<button
className={selectionMode === 'ellipse' ? 'active' : ''}
onClick={() => onSelectionModeChange('ellipse')}
disabled={isProcessing}
>
Ellipse
</button>
<button
className={selectionMode === 'lasso' ? 'active' : ''}
onClick={() => onSelectionModeChange('lasso')}
disabled={isProcessing}
>
Lasso
</button>
</div>
</div>
<div className="control-section">
<h3>AI Mode</h3>
<div className="button-group">
<button
className={mode === 'A' ? 'active' : ''}
onClick={() => onModeChange('A')}
disabled={isProcessing}
title="Mode A: Send only the selected patch (faster, cheaper)"
>
Mode A (Patch Only)
</button>
<button
className={mode === 'B' ? 'active' : ''}
onClick={() => onModeChange('B')}
disabled={isProcessing}
title="Mode B: Send patch + full image for context (better style consistency)"
>
Mode B (With Context)
</button>
</div>
<p className="mode-hint">
{mode === 'A'
? 'Faster and cheaper - sends only the selected area'
: 'Better style consistency - includes full image for context'}
</p>
</div>
<div className="control-section">
<h3>Edge Feathering</h3>
<div className="slider-group">
<input
type="range"
min="0"
max="50"
value={feather}
onChange={(e) => onFeatherChange(parseInt(e.target.value))}
disabled={isProcessing}
/>
<span className="slider-value">{feather}px</span>
</div>
<p className="hint">Smooth blending at selection edges</p>
</div>
<div className="control-section">
<h3>Prompt</h3>
<textarea
value={prompt}
onChange={(e) => onPromptChange(e.target.value)}
placeholder="Describe what to fix or change in the selected area..."
rows={3}
disabled={isProcessing}
/>
</div>
<div className="control-section action-buttons">
<button
className="fix-btn"
onClick={onFix}
disabled={isProcessing || !hasSelection || !prompt.trim()}
>
{isProcessing ? 'Processing...' : 'Fix Selected Area'}
</button>
</div>
<div className="control-section">
<button
className="download-btn"
onClick={onDownload}
disabled={isProcessing}
>
Download Image
</button>
</div>
</div>
);
};
export default Controls;
-209
View File
@@ -1,209 +0,0 @@
.eye-catalog {
background-color: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
padding: 16px;
}
.catalog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.catalog-header h3 {
margin: 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.upload-toggle-btn {
background-color: #00aa00;
color: white;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
}
.upload-toggle-btn:hover {
background-color: #00cc00;
}
.catalog-error {
background-color: #ff4444;
color: white;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.catalog-error button {
background: none;
color: white;
padding: 0;
font-size: 16px;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
padding: 12px;
background-color: #333;
border-radius: 4px;
}
.upload-form input[type="text"],
.upload-form input[type="file"] {
padding: 8px;
border: 1px solid #555;
border-radius: 4px;
background-color: #2a2a2a;
color: white;
font-size: 13px;
}
.upload-btn {
background-color: #0066ff;
color: white;
padding: 10px;
font-weight: 600;
}
.upload-btn:hover:not(:disabled) {
background-color: #0055dd;
}
.patches-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 8px;
max-height: 200px;
overflow-y: auto;
padding: 4px;
}
.no-patches {
grid-column: 1 / -1;
text-align: center;
color: #888;
font-size: 13px;
padding: 20px;
}
.patch-item {
position: relative;
background-color: #333;
border: 2px solid transparent;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
}
.patch-item:hover {
border-color: #666;
}
.patch-item.selected {
border-color: #00ff00;
box-shadow: 0 0 8px rgba(0, 255, 0, 0.3);
}
.patch-item img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
.patch-info {
padding: 4px 6px;
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.patch-name {
font-size: 10px;
font-weight: 600;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.patch-tags {
font-size: 9px;
color: #888;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.delete-patch-btn {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
padding: 0;
background-color: rgba(255, 0, 0, 0.8);
color: white;
font-size: 14px;
line-height: 1;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s;
}
.patch-item:hover .delete-patch-btn {
opacity: 1;
}
.apply-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #444;
}
.selected-preview {
font-size: 13px;
margin-bottom: 10px;
color: #00ff00;
}
.apply-btn {
width: 100%;
background-color: #0066ff;
color: white;
padding: 12px;
font-size: 14px;
font-weight: 600;
}
.apply-btn:hover:not(:disabled) {
background-color: #0055dd;
}
.apply-btn:disabled {
background-color: #333;
color: #666;
}
.apply-hint {
font-size: 11px;
color: #888;
margin-top: 8px;
text-align: center;
}
-193
View File
@@ -1,193 +0,0 @@
import React, { useState, useEffect } from 'react';
import { patchesApi } from '../utils/api';
import './EyeCatalog.css';
const EyeCatalog = ({
projectId,
selection,
feather,
onApply,
isProcessing,
}) => {
const [patches, setPatches] = useState([]);
const [selectedPatch, setSelectedPatch] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [uploadName, setUploadName] = useState('');
const [uploadFile, setUploadFile] = useState(null);
const [uploadTags, setUploadTags] = useState('');
const [error, setError] = useState(null);
// Load patches on mount
useEffect(() => {
loadPatches();
}, []);
const loadPatches = async () => {
try {
const data = await patchesApi.list('eyes');
setPatches(data);
} catch (err) {
console.error('Failed to load patches:', err);
}
};
const handleUpload = async () => {
if (!uploadFile || !uploadName.trim()) {
setError('Please provide a name and select a file');
return;
}
try {
setIsUploading(true);
setError(null);
await patchesApi.create(uploadName.trim(), uploadFile, 'eyes', uploadTags);
await loadPatches();
setShowUpload(false);
setUploadName('');
setUploadFile(null);
setUploadTags('');
} catch (err) {
setError(`Upload failed: ${err.message}`);
} finally {
setIsUploading(false);
}
};
const handleApply = async () => {
if (!selectedPatch || !selection || !projectId) {
setError('Please select an eye and make a selection on the image');
return;
}
try {
setError(null);
await patchesApi.apply(projectId, selectedPatch.id, selection.bbox, feather);
onApply();
setSelectedPatch(null);
} catch (err) {
setError(`Failed to apply: ${err.message}`);
}
};
const handleDelete = async (patchId) => {
if (!window.confirm('Delete this eye from the catalog?')) return;
try {
await patchesApi.delete(patchId);
await loadPatches();
if (selectedPatch?.id === patchId) {
setSelectedPatch(null);
}
} catch (err) {
setError(`Delete failed: ${err.message}`);
}
};
return (
<div className="eye-catalog">
<div className="catalog-header">
<h3>Eye Catalog</h3>
<button
className="upload-toggle-btn"
onClick={() => setShowUpload(!showUpload)}
>
{showUpload ? 'Cancel' : '+ Add Eye'}
</button>
</div>
{error && (
<div className="catalog-error">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
{showUpload && (
<div className="upload-form">
<input
type="text"
placeholder="Eye name (e.g., 'Greek Serene Left')"
value={uploadName}
onChange={(e) => setUploadName(e.target.value)}
/>
<input
type="file"
accept="image/*"
onChange={(e) => setUploadFile(e.target.files[0])}
/>
<input
type="text"
placeholder="Tags (e.g., 'greek,serene,left')"
value={uploadTags}
onChange={(e) => setUploadTags(e.target.value)}
/>
<button
className="upload-btn"
onClick={handleUpload}
disabled={isUploading || !uploadFile || !uploadName.trim()}
>
{isUploading ? 'Uploading...' : 'Upload Eye'}
</button>
</div>
)}
<div className="patches-grid">
{patches.length === 0 ? (
<div className="no-patches">
No eyes in catalog yet. Click "+ Add Eye" to upload.
</div>
) : (
patches.map((patch) => (
<div
key={patch.id}
className={`patch-item ${selectedPatch?.id === patch.id ? 'selected' : ''}`}
onClick={() => setSelectedPatch(patch)}
>
<img
src={patchesApi.getImageUrl(patch.id, true)}
alt={patch.name}
onError={(e) => {
e.target.src = patchesApi.getImageUrl(patch.id, false);
}}
/>
<div className="patch-info">
<span className="patch-name">{patch.name}</span>
{patch.tags && <span className="patch-tags">{patch.tags}</span>}
</div>
<button
className="delete-patch-btn"
onClick={(e) => {
e.stopPropagation();
handleDelete(patch.id);
}}
>
×
</button>
</div>
))
)}
</div>
{selectedPatch && (
<div className="apply-section">
<div className="selected-preview">
<strong>Selected:</strong> {selectedPatch.name}
</div>
<button
className="apply-btn"
onClick={handleApply}
disabled={isProcessing || !selection}
>
{!selection ? 'Draw selection first' : 'Apply to Selection'}
</button>
<p className="apply-hint">
Draw a rectangle/ellipse where you want to place the eye
</p>
</div>
)}
</div>
);
};
export default EyeCatalog;
-131
View File
@@ -1,131 +0,0 @@
.history {
padding: 20px;
background-color: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
max-height: 600px;
overflow-y: auto;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.history-header h3 {
font-size: 14px;
font-weight: 600;
color: #ffffff;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.reset-btn {
background-color: #ff4444;
color: white;
padding: 6px 12px;
font-size: 12px;
}
.reset-btn:hover:not(:disabled) {
background-color: #cc0000;
}
.empty-message {
text-align: center;
color: #666;
padding: 40px 20px;
font-style: italic;
}
.history-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.history-item {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
padding: 12px;
transition: border-color 0.2s;
}
.history-item:hover {
border-color: #555;
}
.edit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.edit-id {
font-weight: 600;
color: #ffffff;
font-size: 13px;
}
.edit-status {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.edit-prompt {
color: #cccccc;
font-size: 13px;
margin-bottom: 8px;
line-height: 1.4;
}
.edit-details {
display: flex;
gap: 12px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.edit-mode,
.edit-type,
.edit-feather {
font-size: 11px;
color: #888;
background-color: #2a2a2a;
padding: 2px 8px;
border-radius: 3px;
}
.edit-date {
font-size: 11px;
color: #666;
margin-bottom: 8px;
}
.revert-btn {
width: 100%;
background-color: #0066ff;
color: white;
padding: 8px 12px;
font-size: 12px;
margin-top: 8px;
}
.revert-btn:hover:not(:disabled) {
background-color: #0055dd;
}
.error-message {
color: #ff4444;
font-size: 11px;
margin-top: 8px;
padding: 6px;
background-color: rgba(255, 68, 68, 0.1);
border-radius: 3px;
}
-82
View File
@@ -1,82 +0,0 @@
import React from 'react';
import './History.css';
const History = ({ edits, onRevert, onReset, isProcessing }) => {
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString();
};
const getStatusColor = (status) => {
switch (status) {
case 'completed':
return '#00aa00';
case 'processing':
return '#ffaa00';
case 'failed':
return '#ff0000';
default:
return '#666';
}
};
return (
<div className="history">
<div className="history-header">
<h3>Edit History</h3>
{edits.length > 0 && (
<button
className="reset-btn"
onClick={onReset}
disabled={isProcessing}
>
Reset to Original
</button>
)}
</div>
{edits.length === 0 ? (
<p className="empty-message">No edits yet</p>
) : (
<div className="history-list">
{edits.map((edit) => (
<div key={edit.id} className="history-item">
<div className="edit-info">
<div className="edit-header">
<span className="edit-id">Edit #{edit.id}</span>
<span
className="edit-status"
style={{ color: getStatusColor(edit.status) }}
>
{edit.status}
</span>
</div>
<p className="edit-prompt">{edit.prompt}</p>
<div className="edit-details">
<span className="edit-mode">Mode {edit.mode}</span>
<span className="edit-type">{edit.selection_type}</span>
<span className="edit-feather">Feather: {edit.feather_px}px</span>
</div>
<p className="edit-date">{formatDate(edit.created_at)}</p>
</div>
{edit.status === 'completed' && (
<button
className="revert-btn"
onClick={() => onRevert(edit.id)}
disabled={isProcessing}
>
Revert to This
</button>
)}
{edit.error_message && (
<p className="error-message">{edit.error_message}</p>
)}
</div>
))}
</div>
)}
</div>
);
};
export default History;
-111
View File
@@ -1,111 +0,0 @@
/* ImageCanvas fills the entire canvas wrapper */
.canvas-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.canvas-container canvas {
display: block;
}
/* Selection hint */
.selection-hint {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: #0088ff;
padding: 6px 12px;
border-radius: 4px;
font-size: 11px;
z-index: 10;
white-space: nowrap;
pointer-events: none;
}
/* Clear selection button */
.clear-selection-btn {
position: absolute;
top: 8px;
right: 8px;
background-color: #cc3333;
color: white;
padding: 6px 12px;
font-size: 11px;
border: none;
border-radius: 3px;
cursor: pointer;
z-index: 10;
}
.clear-selection-btn:hover {
background-color: #dd4444;
}
/* Zoom controls */
.zoom-controls {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 4px;
background-color: rgba(0, 0, 0, 0.8);
padding: 6px 10px;
border-radius: 4px;
z-index: 10;
}
.zoom-controls button {
width: 28px;
height: 28px;
padding: 0;
font-size: 18px;
font-weight: bold;
background-color: #444;
color: white;
border: 1px solid #666;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-controls button:hover {
background-color: #555;
}
.zoom-level {
color: white;
font-size: 12px;
min-width: 45px;
text-align: center;
}
/* Tool mode indicator */
.tool-mode-indicator {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 120, 255, 0.9);
color: white;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
z-index: 10;
white-space: nowrap;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
-704
View File
@@ -1,704 +0,0 @@
import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
import { fabric } from 'fabric';
import './ImageCanvas.css';
const ImageCanvas = forwardRef(({
imageUrl,
onSelectionChange,
selectionMode,
activeTool,
zoom = 100,
onSmartSelect,
isProcessing
}, ref) => {
const canvasRef = useRef(null);
const fabricCanvasRef = useRef(null);
const [currentSelection, setCurrentSelection] = useState(null);
const currentSelectionRef = useRef(null);
const lassoPoints = useRef([]);
const onZoomChangeRef = useRef(onZoomChange);
// Keep ref updated
useEffect(() => {
onZoomChangeRef.current = onZoomChange;
}, [onZoomChange]);
// Expose methods to parent
useImperativeHandle(ref, () => ({
getCanvas: () => fabricCanvasRef.current,
clearSelection: () => clearSelection(),
}));
// Update selection ref when state changes
useEffect(() => {
currentSelectionRef.current = currentSelection;
}, [currentSelection]);
// Initialize canvas
useEffect(() => {
if (!canvasRef.current) return;
const canvas = new fabric.Canvas(canvasRef.current, {
selection: false,
backgroundColor: 'transparent',
preserveObjectStacking: true,
});
fabricCanvasRef.current = canvas;
const handleResize = () => {
const container = canvasRef.current?.parentElement;
if (container) {
const width = container.clientWidth;
const height = container.clientHeight;
canvas.setWidth(width);
canvas.setHeight(height);
// Re-center image if it exists
if (imageRef.current) {
centerImage(canvas, imageRef.current, zoom / 100);
}
canvas.renderAll();
}
};
// Initial resize - use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
handleResize();
});
window.addEventListener('resize', handleResize);
// Mouse wheel zoom
const handleWheel = (opt) => {
const e = opt.e;
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY;
let newZoom = canvas.getZoom();
newZoom *= 0.999 ** delta;
// Clamp zoom between 0.1x and 10x
if (newZoom > 10) newZoom = 10;
if (newZoom < 0.1) newZoom = 0.1;
// Zoom to point under cursor
const pointer = canvas.getPointer(e, true);
canvas.zoomToPoint({ x: pointer.x, y: pointer.y }, newZoom);
setCurrentZoom(newZoom);
if (onZoomChangeRef.current) {
onZoomChangeRef.current(newZoom);
}
};
canvas.on('mouse:wheel', handleWheel);
return () => {
window.removeEventListener('resize', handleResize);
canvas.off('mouse:wheel', handleWheel);
canvas.dispose();
};
}, []); // Empty dependency array - only run once on mount
// Center and scale image
const centerImage = (canvas, img, zoomFactor) => {
if (!img) return;
const padding = 40;
const availableWidth = canvas.width - padding;
const availableHeight = canvas.height - padding;
// Calculate base scale to fit
const fitScale = Math.min(
availableWidth / img.width,
availableHeight / img.height
);
baseScaleRef.current = fitScale;
const scale = fitScale * zoomFactor;
img.scale(scale);
img.set({
left: (canvas.width - img.width * scale) / 2,
top: (canvas.height - img.height * scale) / 2,
});
};
// Apply zoom changes
useEffect(() => {
const canvas = fabricCanvasRef.current;
if (!canvas || !imageRef.current) return;
centerImage(canvas, imageRef.current, zoom / 100);
canvas.renderAll();
}, [zoom]);
// Load image when URL changes
useEffect(() => {
if (!fabricCanvasRef.current || !imageUrl) return;
const canvas = fabricCanvasRef.current;
// Ensure canvas has dimensions before loading image
if (canvas.width === 0 || canvas.height === 0) {
const container = canvasRef.current?.parentElement;
if (container) {
canvas.setWidth(container.clientWidth || 800);
canvas.setHeight(container.clientHeight || 600);
}
}
// Add cache buster to force reload
const cacheBustedUrl = `${imageUrl}?t=${Date.now()}`;
fabric.Image.fromURL(cacheBustedUrl, (img) => {
if (!img) {
console.error('Failed to load image from URL:', cacheBustedUrl);
return;
}
canvas.clear();
// Scale image to fit canvas with padding
const padding = 40;
const availableWidth = (canvas.width || 800) - padding;
const availableHeight = (canvas.height || 600) - padding;
const scale = Math.min(
availableWidth / img.width,
availableHeight / img.height
);
img.scale(scale);
img.set({
left: ((canvas.width || 800) - img.width * scale) / 2,
top: ((canvas.height || 600) - img.height * scale) / 2,
selectable: false,
evented: false,
hoverCursor: 'default',
});
imageRef.current = img;
canvas.add(img);
canvas.sendToBack(img);
centerImage(canvas, img, zoom / 100);
canvas.renderAll();
}, { crossOrigin: 'anonymous' });
}, [imageUrl]);
// Handle tool/mode changes
useEffect(() => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
// Remove all event handlers
canvas.off('mouse:down');
canvas.off('mouse:move');
canvas.off('mouse:up');
canvas.off('object:modified');
canvas.off('object:moving');
canvas.off('object:scaling');
// Set up handlers based on selection mode
if (selectionMode === 'rectangle') {
setupRectangleMode(canvas);
} else if (selectionMode === 'ellipse') {
setupEllipseMode(canvas);
} else if (selectionMode === 'lasso') {
setupLassoMode(canvas);
} else if (selectionMode === 'smart') {
setupSmartSelectMode(canvas);
} else if (selectionMode === 'color') {
setupColorSelectMode(canvas);
} else if (activeTool === 'move') {
setupMoveMode(canvas);
} else if (activeTool === 'pan') {
setupPanMode(canvas);
}
}, [selectionMode, activeTool, onSmartSelect]);
const setupMoveMode = (canvas) => {
// In move mode, allow selecting and moving selection objects
const sel = currentSelectionRef.current;
if (sel) {
sel.set({ selectable: true, evented: true });
canvas.setActiveObject(sel);
}
canvas.on('object:modified', (e) => {
if (e.target && e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target);
}
});
};
const setupPanMode = (canvas) => {
let isPanning = false;
let lastPosX, lastPosY;
canvas.on('mouse:down', (e) => {
isPanning = true;
lastPosX = e.e.clientX;
lastPosY = e.e.clientY;
canvas.setCursor('grabbing');
});
canvas.on('mouse:move', (e) => {
if (!isPanning) return;
const deltaX = e.e.clientX - lastPosX;
const deltaY = e.e.clientY - lastPosY;
canvas.relativePan({ x: deltaX, y: deltaY });
lastPosX = e.e.clientX;
lastPosY = e.e.clientY;
});
canvas.on('mouse:up', () => {
isPanning = false;
canvas.setCursor('grab');
});
canvas.setCursor('grab');
};
const setupSmartSelectMode = (canvas) => {
canvas.on('mouse:down', (e) => {
if (isProcessing) return;
const pointer = canvas.getPointer(e.e);
const img = imageRef.current;
if (!img) return;
// Convert to image coordinates
const imgScale = img.scaleX;
const imgLeft = img.left;
const imgTop = img.top;
const x = Math.round((pointer.x - imgLeft) / imgScale);
const y = Math.round((pointer.y - imgTop) / imgScale);
// Check if click is within image bounds
if (x >= 0 && x < img.width && y >= 0 && y < img.height) {
onSmartSelect?.(x, y);
}
});
canvas.setCursor('crosshair');
};
const setupColorSelectMode = (canvas) => {
canvas.on('mouse:down', (e) => {
if (isProcessing) return;
// TODO: Get pixel color at click position
const pointer = canvas.getPointer(e.e);
console.log('Color select at:', pointer);
});
canvas.setCursor('crosshair');
};
const setupRectangleMode = (canvas) => {
let rect = null;
let isDown = false;
let startX, startY;
canvas.on('mouse:down', (e) => {
// Check if clicking on existing selection
const sel = currentSelectionRef.current;
if (e.target && e.target === sel) {
// Allow moving/transforming
return;
}
// Clear previous selection
if (sel) {
canvas.remove(sel);
setCurrentSelection(null);
}
isDown = true;
isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e);
startX = pointer.x;
startY = pointer.y;
rect = new fabric.Rect({
left: startX,
top: startY,
width: 0,
height: 0,
fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#0088ff',
strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
cornerColor: '#0088ff',
cornerSize: 8,
transparentCorners: false,
borderColor: '#0088ff',
});
canvas.add(rect);
});
canvas.on('mouse:move', (e) => {
if (!isDown || !rect) return;
const pointer = canvas.getPointer(e.e);
const width = pointer.x - startX;
const height = pointer.y - startY;
rect.set({
width: Math.abs(width),
height: Math.abs(height),
left: width < 0 ? pointer.x : startX,
top: height < 0 ? pointer.y : startY,
});
canvas.renderAll();
});
canvas.on('mouse:up', () => {
if (isDown && rect && rect.width > 5 && rect.height > 5) {
isDown = false;
isDrawingRef.current = false;
setCurrentSelection(rect);
canvas.setActiveObject(rect);
updateSelection(rect, 'rectangle');
} else if (isDown && rect) {
// Selection too small, remove it
canvas.remove(rect);
isDown = false;
isDrawingRef.current = false;
}
});
canvas.on('object:modified', (e) => {
if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target);
}
});
};
const setupEllipseMode = (canvas) => {
let ellipse = null;
let isDown = false;
let startX, startY;
canvas.on('mouse:down', (e) => {
const sel = currentSelectionRef.current;
if (e.target && e.target === sel) {
return;
}
if (sel) {
canvas.remove(sel);
setCurrentSelection(null);
}
isDown = true;
const pointer = canvas.getPointer(e.e);
startX = pointer.x;
startY = pointer.y;
ellipse = new fabric.Ellipse({
left: startX,
top: startY,
rx: 0,
ry: 0,
fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#0088ff',
strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
cornerColor: '#0088ff',
cornerSize: 8,
transparentCorners: false,
borderColor: '#0088ff',
});
canvas.add(ellipse);
});
canvas.on('mouse:move', (e) => {
if (!isDown || !ellipse) return;
const pointer = canvas.getPointer(e.e);
const rx = Math.abs(pointer.x - startX) / 2;
const ry = Math.abs(pointer.y - startY) / 2;
ellipse.set({
rx: rx,
ry: ry,
left: Math.min(startX, pointer.x),
top: Math.min(startY, pointer.y),
});
canvas.renderAll();
});
canvas.on('mouse:up', () => {
if (isDown && ellipse && ellipse.rx > 5 && ellipse.ry > 5) {
isDown = false;
setCurrentSelection(ellipse);
canvas.setActiveObject(ellipse);
updateSelection(ellipse, 'ellipse');
} else if (isDown && ellipse) {
canvas.remove(ellipse);
isDown = false;
}
});
canvas.on('object:modified', (e) => {
if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target);
}
});
};
const setupLassoMode = (canvas) => {
let points = [];
let drawingLine = null;
let polygon = null;
canvas.on('mouse:down', (e) => {
const sel = currentSelectionRef.current;
if (e.target && e.target === sel) {
return;
}
if (sel) {
canvas.remove(sel);
setCurrentSelection(null);
}
isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e);
points = [{ x: pointer.x, y: pointer.y }];
drawingLine = new fabric.Polyline(points, {
fill: 'transparent',
stroke: '#0088ff',
strokeWidth: 2,
selectable: false,
evented: false,
});
canvas.add(drawingLine);
});
canvas.on('mouse:move', (e) => {
if (!isDrawingRef.current) return;
const pointer = canvas.getPointer(e.e);
points.push({ x: pointer.x, y: pointer.y });
canvas.remove(drawingLine);
drawingLine = new fabric.Polyline([...points], {
fill: 'transparent',
stroke: '#0088ff',
strokeWidth: 2,
selectable: false,
evented: false,
});
canvas.add(drawingLine);
canvas.renderAll();
});
canvas.on('mouse:up', () => {
if (isDrawingRef.current && points.length > 5) {
isDrawingRef.current = false;
lassoPoints.current = [...points];
canvas.remove(drawingLine);
polygon = new fabric.Polygon(points, {
fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#0088ff',
strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
cornerColor: '#0088ff',
cornerSize: 8,
transparentCorners: false,
borderColor: '#0088ff',
});
canvas.add(polygon);
canvas.setActiveObject(polygon);
setCurrentSelection(polygon);
updateSelection(polygon, 'lasso');
} else if (isDrawingRef.current) {
isDrawingRef.current = false;
canvas.remove(drawingLine);
}
});
canvas.on('object:modified', (e) => {
if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target);
}
});
};
const updateSelection = (selection, type) => {
if (!selection || !imageRef.current) return;
const img = imageRef.current;
const imgScale = img.scaleX;
const imgLeft = img.left;
const imgTop = img.top;
let bbox, selectionData = null;
if (type === 'rectangle') {
bbox = {
x: Math.round((selection.left - imgLeft) / imgScale),
y: Math.round((selection.top - imgTop) / imgScale),
width: Math.round(selection.width / imgScale),
height: Math.round(selection.height / imgScale),
};
} else if (type === 'ellipse') {
bbox = {
x: Math.round((selection.left - imgLeft) / imgScale),
y: Math.round((selection.top - imgTop) / imgScale),
width: Math.round((selection.rx * 2) / imgScale),
height: Math.round((selection.ry * 2) / imgScale),
};
} else if (type === 'lasso') {
const bounds = selection.getBoundingRect();
bbox = {
x: Math.round((bounds.left - imgLeft) / imgScale),
y: Math.round((bounds.top - imgTop) / imgScale),
width: Math.round(bounds.width / imgScale),
height: Math.round(bounds.height / imgScale),
};
const relativePoints = lassoPoints.current.map(p => [
Math.round((p.x - imgLeft) / imgScale) - bbox.x,
Math.round((p.y - imgTop) / imgScale) - bbox.y,
]);
selectionData = { points: relativePoints };
}
onSelectionChange?.({
type,
bbox,
selectionData,
});
};
const updateTransformedSelection = (selection) => {
if (!selection || !imageRef.current) return;
const img = imageRef.current;
const imgScale = img.scaleX;
const imgLeft = img.left;
const imgTop = img.top;
const bounds = selection.getBoundingRect(true);
const bbox = {
x: Math.round((bounds.left - imgLeft) / imgScale),
y: Math.round((bounds.top - imgTop) / imgScale),
width: Math.round(bounds.width / imgScale),
height: Math.round(bounds.height / imgScale),
};
let selectionData = null;
const type = selection.type === 'polygon' ? 'lasso' : (selection.type === 'ellipse' ? 'ellipse' : 'rectangle');
if (type === 'lasso' && lassoPoints.current.length > 0) {
const matrix = selection.calcTransformMatrix();
const transformedPoints = lassoPoints.current.map(p => {
const transformed = fabric.util.transformPoint(
new fabric.Point(p.x, p.y),
matrix
);
return [
Math.round((transformed.x - imgLeft) / imgScale) - bbox.x,
Math.round((transformed.y - imgTop) / imgScale) - bbox.y,
];
});
selectionData = { points: transformedPoints };
}
onSelectionChange?.({
type,
bbox,
selectionData,
});
};
const clearSelection = () => {
const canvas = fabricCanvasRef.current;
const sel = currentSelectionRef.current;
if (sel && canvas) {
canvas.remove(sel);
setCurrentSelection(null);
onSelectionChange?.(null);
}
};
const handleZoomIn = () => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
let newZoom = canvas.getZoom() * 1.2;
if (newZoom > 10) newZoom = 10;
canvas.setZoom(newZoom);
setCurrentZoom(newZoom);
if (onZoomChangeRef.current) {
onZoomChangeRef.current(newZoom);
}
};
const handleZoomOut = () => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
let newZoom = canvas.getZoom() / 1.2;
if (newZoom < 0.1) newZoom = 0.1;
canvas.setZoom(newZoom);
setCurrentZoom(newZoom);
if (onZoomChangeRef.current) {
onZoomChangeRef.current(newZoom);
}
};
const handleZoomReset = () => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
canvas.setZoom(1);
canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
setCurrentZoom(1);
if (onZoomChangeRef.current) {
onZoomChangeRef.current(1);
}
};
return (
<div className="canvas-container">
<canvas ref={canvasRef} />
{currentSelection && (
<button className="clear-selection-btn" onClick={clearSelection}>
Clear
</button>
)}
</div>
);
});
ImageCanvas.displayName = 'ImageCanvas';
export default ImageCanvas;
-152
View File
@@ -1,152 +0,0 @@
.layers-panel {
background-color: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
padding: 12px;
}
.layers-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.layers-header h3 {
margin: 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.flatten-btn {
background-color: #555;
color: white;
padding: 4px 10px;
font-size: 11px;
}
.flatten-btn:hover:not(:disabled) {
background-color: #666;
}
.layers-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 4px;
margin-bottom: 10px;
}
.layer-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background-color: #333;
border-bottom: 1px solid #444;
cursor: pointer;
transition: background-color 0.15s;
}
.layer-item:last-child {
border-bottom: none;
}
.layer-item:hover {
background-color: #3a3a3a;
}
.layer-item.active {
background-color: #0066ff33;
border-left: 3px solid #0066ff;
}
.layer-item.dragging {
opacity: 0.5;
background-color: #444;
}
.layer-visibility {
flex-shrink: 0;
}
.layer-visibility input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
}
.layer-preview {
width: 32px;
height: 32px;
background-color: #222;
border: 1px solid #555;
border-radius: 2px;
overflow: hidden;
flex-shrink: 0;
}
.layer-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.layer-preview.background-preview {
background: repeating-conic-gradient(#444 0% 25%, #333 0% 50%) 50% / 8px 8px;
}
.layer-name {
flex: 1;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.layer-lock {
font-size: 10px;
opacity: 0.5;
}
.layer-drag-handle {
cursor: grab;
opacity: 0.5;
font-size: 10px;
}
.layer-drag-handle:active {
cursor: grabbing;
}
.no-layers-hint {
padding: 16px;
text-align: center;
color: #666;
font-size: 11px;
}
.layers-actions {
display: flex;
gap: 6px;
}
.layer-action-btn {
flex: 1;
background-color: #3a3a3a;
color: #ccc;
padding: 6px 8px;
font-size: 10px;
border: 1px solid #555;
}
.layer-action-btn:hover:not(:disabled) {
background-color: #4a4a4a;
color: white;
}
.layer-action-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
-221
View File
@@ -1,221 +0,0 @@
import React, { useState, useEffect } from 'react';
import { toolsApi } from '../utils/api';
import './Layers.css';
const Layers = ({
projectId,
layers,
setLayers,
activeLayer,
setActiveLayer,
onLayerVisibilityChange,
onFlatten,
isProcessing,
onError,
}) => {
const [draggedLayer, setDraggedLayer] = useState(null);
// Load layers on mount and when projectId changes
useEffect(() => {
if (projectId) {
loadLayers();
}
}, [projectId]);
const loadLayers = async () => {
if (!projectId) return;
try {
const result = await toolsApi.listLayers(projectId);
setLayers(result.layers || []);
} catch (err) {
console.error('Failed to load layers:', err);
}
};
const handleDragStart = (e, index) => {
setDraggedLayer(index);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (e, index) => {
e.preventDefault();
if (draggedLayer === null || draggedLayer === index) return;
// Reorder layers
const newLayers = [...layers];
const [removed] = newLayers.splice(draggedLayer, 1);
newLayers.splice(index, 0, removed);
setLayers(newLayers);
setDraggedLayer(index);
};
const handleDragEnd = () => {
setDraggedLayer(null);
};
const toggleVisibility = (layerId) => {
const layer = layers.find((l) => l.id === layerId);
if (layer) {
layer.visible = !layer.visible;
setLayers([...layers]);
onLayerVisibilityChange?.(layerId, layer.visible);
}
};
const handleFlatten = async () => {
if (!projectId || layers.length === 0) return;
const visibleLayers = layers.filter((l) => l.visible !== false);
const layerOrder = visibleLayers.map((l) => l.id);
await onFlatten(layerOrder);
loadLayers();
};
const handleNewLayer = async () => {
if (!projectId) return;
try {
// Create a new empty transparent layer
const newLayer = {
id: `layer-${Date.now()}`,
name: `Layer ${layers.length + 1}`,
visible: true,
thumbnail: null,
};
setLayers([...layers, newLayer]);
setActiveLayer(newLayer.id);
} catch (err) {
onError?.(`Failed to create layer: ${err.message}`);
}
};
const handleDeleteLayer = async () => {
if (!projectId || activeLayer === 'background') return;
try {
const updatedLayers = layers.filter((l) => l.id !== activeLayer);
setLayers(updatedLayers);
setActiveLayer(updatedLayers.length > 0 ? updatedLayers[updatedLayers.length - 1].id : 'background');
} catch (err) {
onError?.(`Failed to delete layer: ${err.message}`);
}
};
const handleDuplicateLayer = async () => {
if (!projectId || activeLayer === 'background') return;
try {
const layerToDuplicate = layers.find((l) => l.id === activeLayer);
if (!layerToDuplicate) return;
const newLayer = {
...layerToDuplicate,
id: `layer-${Date.now()}`,
name: `${layerToDuplicate.name} copy`,
};
const activeIndex = layers.findIndex((l) => l.id === activeLayer);
const updatedLayers = [...layers];
updatedLayers.splice(activeIndex + 1, 0, newLayer);
setLayers(updatedLayers);
setActiveLayer(newLayer.id);
} catch (err) {
onError?.(`Failed to duplicate layer: ${err.message}`);
}
};
return (
<div className="layers-panel">
<div className="layers-header">
<h3>Layers</h3>
{layers.length > 0 && (
<button
className="flatten-btn"
onClick={handleFlatten}
disabled={isProcessing}
title="Merge all visible layers"
>
Flatten
</button>
)}
</div>
<div className="layers-list">
{/* Background layer (always present) */}
<div
className={`layer-item ${activeLayer === 'background' ? 'active' : ''}`}
onClick={() => setActiveLayer('background')}
>
<span className="layer-visibility">
<input type="checkbox" checked disabled />
</span>
<span className="layer-preview background-preview"></span>
<span className="layer-name">Background</span>
<span className="layer-lock">🔒</span>
</div>
{/* Dynamic layers */}
{layers.map((layer, index) => (
<div
key={layer.id}
className={`layer-item ${activeLayer === layer.id ? 'active' : ''} ${
draggedLayer === index ? 'dragging' : ''
}`}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
onClick={() => setActiveLayer(layer.id)}
>
<span className="layer-visibility">
<input
type="checkbox"
checked={layer.visible !== false}
onChange={() => toggleVisibility(layer.id)}
onClick={(e) => e.stopPropagation()}
/>
</span>
<span className="layer-preview">
{layer.thumbnail && (
<img src={layer.thumbnail} alt={layer.name} />
)}
</span>
<span className="layer-name">{layer.name}</span>
<span className="layer-drag-handle"></span>
</div>
))}
{layers.length === 0 && (
<div className="no-layers-hint">
Use "Remove Background" to create layers
</div>
)}
</div>
<div className="layers-actions">
<button
className="layer-action-btn"
disabled={isProcessing || !projectId}
title="Add new empty layer"
onClick={handleNewLayer}
>
+ New Layer
</button>
<button
className="layer-action-btn"
disabled={isProcessing || activeLayer === 'background'}
title="Delete selected layer"
onClick={handleDeleteLayer}
>
Delete
</button>
<button
className="layer-action-btn"
disabled={isProcessing || activeLayer === 'background'}
title="Duplicate selected layer"
onClick={handleDuplicateLayer}
>
Duplicate
</button>
</div>
</div>
);
};
export default Layers;
+732
View File
@@ -0,0 +1,732 @@
/*****************\
| UI Button Group |
\*****************/
.ui_button_group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.ui_button_group.no_wrap {
flex-wrap: nowrap;
}
.ui_button_group.stacked {
margin: .75rem 0;
}
.ui_button_group.stacked:first-child {
margin-top: 0;
}
.ui_button_group.stacked:last-child {
margin-bottom: 0;
}
.ui_button_group > button,
.ui_button_group > input[type="button"] {
border-radius: 0;
}
.ui_button_group > button:focus,
.ui_button_group > input[type="button"]:focus {
z-index: 1;
}
.ui_button_group > button + button,
.ui_button_group > button + input[type="button"],
.ui_button_group > input[type="button"] + button,
.ui_button_group > input[type="button"] + input[type="button"] {
margin-left: -1px;
}
.ui_button_group > button:first-child,
.ui_button_group > input[type="button"]:first-child {
border-radius: var(--button-border-radius) 0 0 var(--button-border-radius);
}
.ui_button_group > button:last-child,
.ui_button_group > input[type="button"]:last-child {
border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
}
/****************\
| UI Color Input |
\****************/
.ui_color_input {
display: inline-block;
padding: 0;
margin: 0;
position: relative;
overflow: hidden;
vertical-align: middle;
}
.ui_color_input input[type="color"] {
display: block;
cursor: pointer;
padding: 0;
border: .2rem solid var(--input-background-color);
width: 3rem;
}
.ui_color_input .alpha_overlay {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9bRdGKiBlERDJUJwuiIo5ahSJUCLVCqw4mL/2DJg1Jiouj4Fpw8Gex6uDirKuDqyAI/oA4OTopukiJ9yWFFjFeeLyP8+45vHcfEKyVmGa1jQOabpvJeExMZ1bFjld0YxgBCOiTmWXMSVICvvV1T51Ud1Ge5d/3Z/WoWYsBAZF4lhmmTbxBPL1pG5z3iQVWkFXic+Ixky5I/Mh1xeM3znmXgzxTMFPJeWKBWMy3sNLCrGBqxFPEEVXTKT+Y9ljlvMVZK1VY4578heGsvrLMdVpDiGMRS5AgQkEFRZRgI0q7ToqFJJ3HfPyDrl8il0KuIhg5FlCGBtn1g//B79lauckJLykcA9pfHOdjBOjYBepVx/k+dpz6CRB6Bq70pr9cA2Y+Sa82tcgR0LsNXFw3NWUPuNwBBp4M2ZRdKUQrmMsB72f0TRmg/xboWvPm1jjH6QOQolklboCDQ2A0T9nrPu/ubJ3bvz2N+f0AL+pyjMZuudYAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfkCx4BHwaj7CMVAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAC5JREFUOMtjfPfuHQNuICgoiEeWiYECMKp5ZGhm/P//Px7p9+/fjwbYqGZKNAMAANAI7r7rfkQAAAAASUVORK5CYII=');
background-size: 100% 100%;
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
pointer-events: none;
}
/**************************\
| UI Color Picker Gradient |
\**************************/
.ui_color_picker_gradient {
padding: 0 0 80% 0;
position: relative;
width: 100%;
}
.ui_color_picker_gradient .primary_pick {
position: absolute;
left: 86%;
right: 0;
top: 0;
bottom: 0;
background: white;
}
.ui_color_picker_gradient .secondary_pick {
position: absolute;
left: 0;
right: 17%;
top: 0;
bottom: 0;
border: 1px solid var(--border-color);
background: green;
}
.ui_color_picker_gradient .secondary_pick:focus {
outline: 0;
border: 1px solid var(--input-border-color-active);
}
.ui_color_picker_gradient .secondary_pick .saturation_gradient {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
}
.ui_color_picker_gradient .secondary_pick .value_gradient {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
}
.ui_color_picker_gradient .secondary_pick .handle {
position: absolute;
left: 0;
right: auto;
top: 0;
bottom: auto;
pointer-events: none;
}
.ui_color_picker_gradient .secondary_pick .handle:before {
content: '';
display: block;
position: absolute;
left: -.6rem;
top: -.6rem;
height: .3rem;
width: .3rem;
border: .4rem solid #999;
border-radius: 1000px;
}
.ui_color_picker_gradient .secondary_pick .handle:after {
content: '';
display: block;
position: absolute;
left: -.5rem;
top: -.5rem;
height: .5rem;
width: .5rem;
border: .2rem solid white;
border-radius: 1000px;
}
.ui_color_picker_gradient .primary_pick .ui_range {
border-color: rgba(1, 1, 1, 0.1);
}
.ui_color_picker_gradient .primary_pick .ui_range:focus {
border-color: var(--input-border-color-active);
}
/*****************\
| UI Color Sample |
\*****************/
.ui_color_sample {
border: 1px solid #999;
box-shadow: 0 0 0 1px #555 inset;
display: block;
height: 28px;
width: 28px;
}
/***************\
| UI Flex Group |
\***************/
.ui_flex_group {
display: flex;
flex-direction: row;
}
.ui_flex_group.stacked {
margin: .75rem 0;
}
.ui_flex_group.stacked:first-child {
margin-top: 0;
}
.ui_flex_group.stacked:last-child {
margin-bottom: 0;
}
.ui_flex_group.column {
flex-direction: column;
}
.ui_flex_group.justify_content_center {
justify-content: center;
}
.ui_flex_group.justify_content_start {
justify-content: flex-start;
}
.ui_flex_group.justify_content_end {
justify-content: flex-end;
}
.ui_flex_group.justify_content_space_around {
justify-content: space-around;
}
.ui_flex_group.justify_content_space_between {
justify-content: space-between;
}
.ui_flex_group.align_items_baseline {
align-items: baseline;
}
.ui_flex_group.align_items_center {
align-items: center;
}
.ui_flex_group.align_items_start {
align-items: flex-start;
}
.ui_flex_group.align_items_end {
align-items: flex-end;
}
.ui_flex_group.align_items_stretch {
align-items: stretch;
}
/****************\
| UI Icon Button |
\****************/
.ui_icon_button {
height: 2.8rem;
line-height: 2.8rem;
}
.ui_icon_button.input_height {
height: 2.4rem;
line-height: 2.4rem;
}
.ui_icon_button > svg {
display: block;
font-size: 1.6rem;
}
.ui_icon_button > img {
display: block;
margin: 0 auto;
}
button img{
filter: var(--menu-icons-filter);
}
/****************\
| UI Input Group |
\****************/
.ui_input_group {
display: flex;
flex-direction: row;
min-height: 2.4rem;
width: 100%;
}
.ui_input_group.stacked {
margin: .75rem 0;
}
.ui_input_group.stacked:first-child {
margin-top: 0;
}
.ui_input_group.stacked:last-child {
margin-bottom: 0;
}
.ui_input_group > input,
.ui_input_group > .ui_number_input,
.ui_input_group > .ui_range,
.ui_input_group > .ui_color_sample {
border-radius: 0;
height: auto;
min-width: 0;
}
.ui_input_group > .ui_color_sample {
border: none;
width: 100%;
}
.ui_input_group > :first-child {
border-radius: var(--input-border-radius) 0 0 var(--input-border-radius);
}
.ui_input_group > :last-child {
border-radius: 0 var(--input-border-radius) var(--input-border-radius) 0;
}
.ui_input_group > label {
display: flex;
align-items: center;
border: 1px solid var(--input-group-border-color);
border-right: 0;
margin: 0;
padding: 0 .75rem;
}
.ui_input_group > .ui_range + input,
.ui_input_group > .ui_range + .ui_number_input {
margin-left: -1px;
}
.ui_input_grid {
border-radius: var(--input-border-radius);
box-shadow: 0 1px 0 0 rgba(1, 1, 1, 0.1);
}
.ui_input_grid.stacked {
margin: .75rem 0;
}
.ui_input_grid.stacked:first-child {
margin-top: 0;
}
.ui_input_grid.stacked:last-child {
margin-bottom: 0;
}
:not(.ui_input_grid) > .ui_input_group {
border-radius: var(--input-border-radius);
box-shadow: 0 1px 0 0 rgba(1, 1, 1, 0.1);
}
.ui_input_grid > .ui_input_group {
margin: -1px 0;
}
.ui_input_grid > .ui_input_group > :first-child,
.ui_input_grid > .ui_input_group > :last-child {
border-radius: 0;
}
.ui_input_grid > .ui_input_group:first-child {
margin-top: 0;
}
.ui_input_grid > .ui_input_group:first-child > :first-child {
border-radius: var(--input-border-radius) 0 0 0;
}
.ui_input_grid > .ui_input_group:first-child > :last-child {
border-radius: 0 var(--input-border-radius) 0 0;
}
.ui_input_grid > .ui_input_group:last-child {
margin-bottom: 0;
}
.ui_input_grid > .ui_input_group:last-child > :first-child {
border-radius: 0 0 0 var(--input-border-radius);
}
.ui_input_grid > .ui_input_group:last-child > :last-child {
border-radius: 0 0 var(--input-border-radius) 0;
}
/*****************\
| UI Number Input |
\*****************/
.ui_number_input {
border: 1px solid var(--input-border-color);
border-radius: var(--input-border-radius);
display: inline-block;
padding: 0;
margin: 0;
position: relative;
overflow: hidden;
vertical-align: middle;
}
.ui_number_input > input[type="number"]::-webkit-outer-spin-button,
.ui_number_input > input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.ui_number_input > input[type="number"] {
border: none;
border-radius: 0;
-moz-appearance: textfield;
appearance: textfield;
padding-right: 2.5rem;
padding-right: calc(var(--number-input-arrow-width) + .5rem);
width: 100%;
}
.ui_number_input > .increase_number,
.ui_number_input > .decrease_number {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 2rem;
width: var(--number-input-arrow-width);
border-radius: 0;
border: 1px solid var(--input-border-color);
border-right: none;
padding: 0;
margin: 0;
}
.ui_number_input > ::-moz-focus-inner {
border: 0;
}
.ui_number_input > .increase_number:focus,
.ui_number_input > .decrease_number:focus {
outline: 0;
}
.ui_number_input > .increase_number {
right: 0;
top: 0;
bottom: 50%;
border-top: none;
}
.ui_number_input > .increase_number::after {
content: '';
display: block;
width: 0;
height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-bottom: 3px solid var(--input-text-color);
}
.ui_number_input > .decrease_number {
right: 0;
top: calc(50% - 1px);
bottom: 0;
border-bottom: none;
}
.ui_number_input > .decrease_number::after {
content: '';
display: block;
width: 0;
height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 3px solid var(--input-text-color);
}
/**********\
| UI Range |
\**********/
:root {
--range-handle-width: 18px;
}
.ui_range {
display: flex;
flex-direction: row;
background: var(--input-background-color);
border: 1px solid var(--input-border-color);
border-radius: 1000px;
height: 1.8rem;
overflow: visible;
outline: 0;
padding: 0 calc(var(--range-handle-width) / 2);
position: relative;
width: 100%;
}
.ui_range:focus {
border-color: var(--input-border-color-active);
z-index: 1;
}
.ui_range.active {
cursor: col-resize;
}
.ui_range .padded_track {
position: absolute;
left: calc(var(--range-handle-width) / 2);
right: calc(var(--range-handle-width) / 2);
top: 0;
bottom: 0;
}
.ui_range .bar {
overflow: visible;
position: relative;
width: 0%;
}
.ui_range .handle {
background: var(--input-text-color);
border: 1px solid var(--border-color);
border-radius: 1000px;
box-sizing: border-box;
cursor: col-resize;
display: block;
height: 1.8rem;
width: var(--range-handle-width);
position: absolute;
top: 50%;
right: 0;
transform: translate(50%, -50%);
}
.ui_range.color_picker .handle {
background: none;
border: none;
border-radius: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: auto;
top: 0;
bottom: 0;
transform: translateX(50%);
}
.ui_range.color_picker .handle::before {
content: '';
display: block;
width: 0;
height: 0;
border-left: .5rem solid transparent;
border-right: .5rem solid transparent;
border-top: .7rem solid white;
}
.ui_range.color_picker .handle::after {
content: '';
display: block;
width: 0;
height: 0;
border-left: .5rem solid transparent;
border-right: .5rem solid transparent;
border-bottom: .7rem solid black;
}
.ui_range.color_picker .handle:hover::before {
border-top-color: #eaeaea;
}
.ui_range.color_picker .handle:hover::after {
border-bottom-color: #222;
}
.ui_range.vertical {
flex-direction: column;
justify-content: flex-end;
height: 100%;
width: 1.8rem;
padding: calc(var(--range-handle-width) / 2) 0;
}
.ui_range.vertical.active {
cursor: row-resize;
}
.ui_range.vertical .padded_track {
left: 0;
right: 0;
top: calc(var(--range-handle-width) / 2);
bottom: calc(var(--range-handle-width) / 2);
}
.ui_range.vertical .bar {
width: 100%;
height: 0%;
}
.ui_range.vertical .handle {
transform: translate(50%, -50%);
top: 0;
right: 50%;
cursor: row-resize;
}
.ui_range.vertical.color_picker_thin {
padding: 1px 0;
border-radius: 0;
width: 100%;
}
.ui_range.vertical.color_picker_thin .padded_track {
top: 0;
bottom: 0;
}
.ui_range.vertical.color_picker_thin .handle {
border-radius: 0;
width: 100%;
height: .5rem;
}
/*************\
| UI Swatches |
\*************/
.ui_swatches {
display: flex;
justify-content: center;
}
.ui_swatches .swatch_group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: auto;
border-radius: var(--input-border-radius);
border: 1px solid var(--border-color);
border-right: transparent;
box-shadow: 0 1px 0 0 rgba(1, 1, 1, 0.1);
overflow: hidden;
max-height: calc(2.3rem);
}
.ui_swatches .swatch_group:focus {
outline: 0;
box-shadow: 0 0 0 1px var(--input-border-color-active);
}
.ui_swatches .swatch_group.rows_2 {
max-height: calc(4.6rem - 1px);
}
.ui_swatches .swatch_group.rows_3 {
max-height: calc(6.9rem - 2px);
}
.ui_swatches .swatch_group.cols_1 .swatch {
width: 100%;
}
.ui_swatches .swatch_group.cols_2 .swatch {
width: 50%;
}
.ui_swatches .swatch_group.cols_3 .swatch {
width: 33.33%;
}
.ui_swatches .swatch_group.cols_4 .swatch {
width: 25%;
}
.ui_swatches .swatch_group.cols_5 .swatch {
width: 20%;
}
.ui_swatches .swatch_group.cols_6 .swatch {
width: 16.66%;
}
.ui_swatches .swatch_group.cols_7 .swatch {
width: 14.29%;
}
.ui_swatches .swatch_group.cols_8 .swatch {
width: 12.5%;
}
.ui_swatches .swatch {
background: white;
display: inline-block;
position: relative;
border: 1px solid var(--border-color);
border-radius: 0;
box-shadow: 0 0 0 1px white inset;
margin: -1px 0 0 -1px;
padding: 0;
height: 2.3rem;
min-width: 2.3rem;
flex-grow: 1;
}
.ui_swatches .swatch:hover,
.ui_swatches .swatch:focus {
background: white;
box-shadow: 0 0 0 2px white inset, 0 0 0 3px var(--border-color) inset;
}
.ui_swatches .swatch:hover:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 50%, rgba(0, 0, 0, 0.1) 51%, rgba(0, 0, 0, 0.1) 100%);
}
.ui_swatches .swatch.active {
box-shadow: 0 0 0 3px var(--button-text-color-active) inset, 0 0 0 4px var(--border-color) inset;
}
/******************\
| UI Toggle Button |
\******************/
.ui_toggle_button {
padding-left: 2.6rem !important;
position: relative;
}
.ui_toggle_button:before {
background-color: var(--button-toggle-background-color);
background-image: url('data:image/svg+xml;utf8,<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="white" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>');
background-position: center;
background-repeat: no-repeat;
border-radius: var(--button-border-radius) 0 0 var(--button-border-radius);
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1.8rem;
content: '';
}
.ui_toggle_button[aria-pressed="true"]:before {
background-color: var(--button-text-color-active);
background-image: url('data:image/svg+xml;utf8,<svg width="0.7em" height="1em" viewBox="0 0 16 16" class="bi bi-check2" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>');
}
/* media */
.media-paging{
width: 100%;
margin: 10px 0;
text-align: center;
}
.media-paging button{
background-color: var(--button-background-color);
color: var(--text-color);
}
.media-paging button.selected{
background-color: var(--background-color-active);
color: var(--text-color-active);
}
/* global search */
#global_search_results{
padding-top: 10px;
font-size: 14px;
}
#global_search_results .search-result {
padding: 3px 5px;
}
#global_search_results .search-result.active{
background-color: var(--background-color-active);
color: var(--text-color-active);
border-radius: 2px;
}
#global_search_results b{
color: var(--text-color-red);
}
.popup.shortcuts table{
line-height: 1;
}
+769
View File
@@ -0,0 +1,769 @@
.wrapper{
display: -ms-grid;
display: grid;
margin: 0;
position: fixed; /* dont change it, vh does not work on mobiles with bottom footer */
top: 30px;
right: 0;
left: 0;
bottom: 5px;
height: auto;
overflow: hidden;
-ms-grid-rows: auto 1fr;
grid-template-rows: auto 1fr;
-ms-grid-columns: auto 1fr auto;
grid-template-columns: auto 1fr auto;
grid-template-areas:
"submenu submenu submenu"
"sidebar_left main sidebar_right";
}
.trn{}
.toggle{
cursor: pointer;
}
.hidden{
display:none;
}
.center{
text-align: center;
}
.pointer{
cursor: pointer;
}
.clear{
clear:both;
}
.displayBlock{
display: block;
}
.bold{
font-weight: bold;
}
.left{
float: left;
}
.right{
float: right;
}
.grey{
color:grey;
}
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version */
}
.block{
position: relative;
background-color: rgba(255, 255, 255, 0.2);
background-color: var(--block-background-color);
border: 1px solid rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
margin-bottom: 10px;
user-select: none;
border-radius: 4px;
}
.sidebar_right .block{
background-color: #68727b;
background-color: var(--block-background-color);
border-bottom: none;
box-shadow: 0 -2px 0 0 var(--header-background-color) inset;
}
.block:last-child{
margin-bottom: 0;
}
.block h2{
position: relative;
padding: 2px 5px 2px 6px;
margin: 0;
font-size: 110%;
background-color: rgba(255, 255, 255, 0.3);
background-color: var(--header-background-color);
border-bottom: #555;
border-radius: 4px 4px 0 0;
}
.block.toggled h2, .block h2.toggled:after{
border: none;
}
.block h2.toggle:before{
/* icon */
position:absolute;
content:'';
width: 0;
height: 0;
right: 10px;
top: 10px;
border-style: solid;
border-width: 0 4px 5px 4px;
border-color: transparent transparent var(--text-color-muted) transparent;
}
.block h2.toggled:before{
/* icon */
border-width: 5px 4px 0 4px;
border-color: var(--text-color-muted) transparent transparent transparent;
}
.block .content{
padding: 7.5px 5px;
}
.block_section {
margin: .75rem 0;
}
.block_section:first-child {
margin-top: 0;
}
.block_section:last-child {
margin-bottom: 0;
}
.error{
padding:20px;
margin:10px;
border:1px solid #ff0000;
background-color:#ffffff;
width:500px;
font-weight:bold;
}
/* color chooser */
body .sp-replacer{
width: 100%;
height: 40px;
}
body .sp-preview{
width: calc(100% - 20px);
height: 100%;
}
/* ========== header ======================================================== */
.logo{
position: relative;
display: inline-block;
height: 30px;
width: 110px;
padding: 5px 5px 5px 36px;
margin: 5px;
font-size: 14px;
text-decoration: none;
font-weight: bold;
color: #ffffff;
color: var(--text-color);
}
.logo:after{
position:absolute;
content:"";
left: 0;
top: 0;
width: 31px;
height: 30px;
background: url('images/logo.svg') no-repeat center center;
background-size: auto 28px;
filter: var(--mobile-menu-toggle-filter);
}
.logo:hover:after{
left: 2px;
}
.about-logo{
margin-left:22%;
}
.about-name{
font-size:15px;
font-weight:bold;
}
.undo_button {
display: none;
width: 50px;
height: 50px;
top: 0;
border: 0;
outline: none;
cursor: pointer;
filter: var(--mobile-menu-toggle-filter);
background: url(images/icons/undo.svg) no-repeat center center;
background-size: auto 25px;
margin-left: 10px;
}
.undo_button:hover {
background-color: transparent;
}
@media screen and (max-width: 700px){
.undo_button {
display: block;
}
}
/* ========== sub-header ==================================================== */
.submenu{
-ms-grid-row: 1;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
grid-area: submenu;
display: flex;
flex-direction: row;
align-items: center;
background-color: rgba(255, 255, 255, 0.2);
background-color: var(--section-background-color);
overflow: hidden;
margin-bottom: 5px;
}
.attributes{
display: flex;
flex-wrap: nowrap;
background-color: var(--area-background-color);
width: calc(100% - 125px);
margin-top: 5px;
margin-bottom: 5px !important;
padding: 3px 10px 3px 10px;
border: 0;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
min-height: 30px;
}
.attributes .item{
display: inline-flex;
align-items: center;
margin-right: 20px;
}
.attributes .item > label {
margin: 0 .5rem 0 0;
}
.attributes input[type="number"]{
width: 60px;
margin-right: 5px;
}
.attributes input[type="color"] {
cursor: pointer;
padding: 0;
border: .2rem solid var(--input-background-color);
width: 3rem;
}
.attributes .item > button:not(.ui_icon_button){
display: inline-block;
padding: 3px 10px;
}
/* ========== left sidebar ================================================== */
.sidebar_left{
-ms-grid-row: 2;
-ms-grid-column: 1;
grid-area: sidebar_left;
display: flex;
flex-direction: row;
flex-wrap: wrap;
background-color: var(--section-background-color);
padding: 0 5px 5px 0;
margin-right: 5px;
overflow: hidden;
align-self: start;
width: 40px;
overflow-y: auto;
max-height: 100%;
}
.sidebar_left .item{
position: relative;
display:block;
background-color: var(--area-background-color);
height: 25px;
width: 30px;
margin: 5px 0 0 5px;
overflow: hidden;
cursor: pointer;
}
.sidebar_left .item:after{
position: absolute;
content: '';
left:0;
top:0;
bottom:0;
right:0;
filter: var(--menu-icons-filter);
background-position: center center;
background-repeat: no-repeat;
background-size: 20px 20px;
}
.sidebar_left .item:hover{
background-color: var(--background-color-hover);
}
.sidebar_left .item.active{
background-color: var(--background-color-active);
color: var(--text-color-active);
}
.sidebar_left .item.active:after{
filter: var(--menu-icons-filter-active);
}
/*
IMPORTANT: any new icon should also must be added on /service-worker.js + its version should be updated - FEATURE DISABLED
*/
.sidebar_left .select:after{ background-image: url('images/icons/select.svg'); }
.sidebar_left .selection:after{ background-image: url('images/icons/selection.svg'); }
.sidebar_left .brush:after{ background-image: url('images/icons/brush.svg'); }
.sidebar_left .pencil:after{ background-image: url('images/icons/pencil.svg'); }
.sidebar_left .pick_color:after{ background-image: url('images/icons/pick_color.svg'); }
.sidebar_left .erase:after{ background-image: url('images/icons/erase.svg'); }
.sidebar_left .magic_erase:after{ background-image: url('images/icons/magic_erase.svg'); }
.sidebar_left .fill:after{ background-image: url('images/icons/fill.svg'); }
.sidebar_left .media:after{ background-image: url('images/icons/media.svg'); }
.sidebar_left .shape:after{ background-image: url('images/icons/shape.svg'); }
.sidebar_left .text:after{ background-image: url('images/icons/text.svg'); background-size: 16px auto; }
.sidebar_left .gradient:after{ background-image: url('images/icons/gradient.png'); background-size: 18px 12px; filter: none; }
.sidebar_left .clone:after{ background-image: url('images/icons/clone.svg'); }
.sidebar_left .crop:after{ background-image: url('images/icons/crop.svg'); }
.sidebar_left .blur:after{ background-image: url('images/icons/blur.svg'); }
.sidebar_left .sharpen:after{ background-image: url('images/icons/sharpen.svg'); }
.sidebar_left .desaturate:after{ background-image: url('images/icons/desaturate.svg'); }
.sidebar_left .bulge_pinch:after{ background-image: url('images/icons/bulge_pinch.svg'); }
.sidebar_left .animation:after{ background-image: url('images/icons/animation.svg'); }
@media screen and (max-width:550px){
#sidebar_left{
left: -110px;
}
}
/* ========== right sidebar ================================================= */
.sidebar_right{
-ms-grid-row: 2;
-ms-grid-column: 3;
grid-area: sidebar_right;
z-index: 2;
display: flex;
flex-direction: column;
transition: 0.2s;
overflow-x: hidden;
overflow-y: scroll;
margin: 0 5px;
width: 200px;
}
.sidebar_right.active{
right: 0 !important;
}
.sidebar_right .block.layers{
flex: 1;
}
.sidebar_right .block.layers .content{
padding-bottom: 25px;
}
/* preview */
.canvas_preview_wrapper{
position:relative;
height:100px;
margin: 5px 5px 10px 5px;
}
.canvas_preview_details{
padding: 0 5px;
}
.canvas_preview_details button{
margin: 0;
}
.preview canvas{
cursor: pointer;
}
.details input{
padding: 5px 10px;
}
/* color */
.color_area{
border: 1px solid #444;
width: calc(100% - 10px);
height: 40px;
cursor: pointer;
margin: 5px;
}
/* layers */
.layers_list{
margin-top: 10px;
}
.layers_arrow{
display:inline-block;
float:right;
margin-left:5px;
padding:1px 8px;
border:1px solid #444;
border-color: var(--border-color);
text-decoration:none;
color:var(--text-color);
font-size:12px;
}
.layer_add{
display:inline-block;
padding:1px 8px;
margin-right: 10px;
background-color: #419147;
background-color: var(--background-color-active);
border:1px solid #444;
border-color: var(--border-color);
color: var(--text-color-active);
cursor:pointer;
text-decoration:none;
}
.layers_list .item{
margin-bottom:2px;
}
.layers_list .layer_name{
display:block;
padding:1px 5px 3px 5px;
height:19px;
width: calc(100% - 44px);
text-align: left;
overflow:hidden;
background-color:#989898;
background-color: var(--area-background-color);
border:1px solid #393939;
border-color: var(--border-color);
border-radius:3px;
cursor:pointer;
overflow:hidden;
font-size: 12px;
color:var(--text-color);
white-space: nowrap;
}
.layers_list .item.shorter .layer_name{
width: calc(100% - 63px);
}
.layers_list .item.active .layer_name{
background-color: var(--background-color-active);
color: var(--text-color-active);
}
.layers_list .arrow_down{
position: relative;
float:left;
margin-right: 5px;
width:10px;
height:19px;
opacity: 0.4;
}
.layers_list .arrow_down:after{
position: absolute;
content: '';
left:0;
top:0;
bottom:0;
right:0;
filter: var(--menu-icons-filter);
background: url('images/icons/arrow-down.svg') no-repeat center center;
background-size: 12px auto;
}
.layers_list .visibility{
position: relative;
float:left;
cursor:pointer;
padding:0px 3px 0px 3px;
margin-right: 5px;
width:20px;
height:19px;
opacity:0.1;
border: none;
background: transparent;
box-shadow: none;
}
.layers_list .visibility:after{
position: absolute;
content: '';
left:0;
top:0;
bottom:0;
right:0;
filter: var(--menu-icons-filter);
background: url('images/icons/view.svg') no-repeat center center;
background-size: 18px auto;
}
.layers_list .visible{ opacity:0.4; }
.layers_list .delete{
float:right;
cursor:pointer;
padding:0px 3px 0px 3px;
width:12px;
height:19px;
margin-left: 5px;
background: transparent url(images/icons/delete.svg) no-repeat center center;
background-size: 10px 10px;
border: none;
box-shadow: none;
}
/* filters */
.layers_list .filters{
margin-bottom: 5px;
}
.layers_list .filter{
margin-bottom: 2px;
margin-left: 30px;
opacity: 0.7;
}
.layers_list .filter .layer_name{
position: relative;
}
.layers_list .filter .layer_name:after{
position:absolute;
content:"fx";
right: -4px;
top:1px;
bottom:0;
width: 20px;
}
.sidebar_right .label{
display: inline-block;
}
.info .toggle.toggled{
margin-bottom: -3px;
}
.block.details .row{
clear:both;
margin-bottom: 4px;
min-height: 23px;
}
.block.details input[type="number"]{
width: 70px;
padding: 3px 5px;
float: right;
}
.block.details .ui_color_input{
width: 70px;
float: right;
}
.block.details .ui_color_input input{
width: 100%;
height: 23px;
}
.block.details button.ui_toggle_button{
width: 90px;
float: right;
}
.block.details select{
width: calc(100% - 70px);
height: 23px;
float: right;
}
.block.details button{
width: calc(100% - 70px);
height: 23px;
border: 1px solid #444;
}
.block.details button.reset{
position: relative;
width: 25px;
float: right;
margin-right: 3px;
overflow: hidden;
opacity: 0.5;
color: transparent;
}
.block.details button.reset:after{
position: absolute;
content: '';
left:0;
top:0;
bottom:0;
right:0;
background: url(images/icons/refresh.svg) no-repeat center center;
background-size: auto 14px;
filter: var(--menu-icons-filter);
}
.block.details button.active{
background-color: var(--background-color-active);
color: var(--text-color-active);
}
.details-content{
height: 206px;
overflow-y: auto;
}
@media screen and (max-width:700px){
body{
padding-top:50px;
}
.wrapper{
top: 50px;
}
.sidebar_left{
position: absolute;
left: -90px;
background: var(--background);
}
.sidebar_left.active{
box-shadow: -5px 0px 10px 0px rgba(0,0,0,0.75);
left: 0;
z-index: 3;
}
.sidebar_right{
position: absolute;
height: 100%;
right: -210px;
background: var(--background);
}
.sidebar_right.active{
box-shadow: -5px 0px 10px 0px rgba(0,0,0,0.75);
right: 0;
margin-right: 0;
}
}
/* ========== content ======================================================= */
.ruler_left{
display: none;
position: absolute;
left:0;
top: 20px;
background-color: #ccc;
}
.ruler_top{
display: none;
position: absolute;
left: 20px;
top:0;
background-color: #ccc;
}
.middle_area{
position: relative;
-ms-grid-row: 2;
-ms-grid-column: 2;
grid-area: main;
}
.main_wrapper{
position:absolute;
top:0;
right:0;
bottom:0;
left:0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.middle_area.has-ruler .main_wrapper{
top: 20px;
left: 20px;
}
.canvas_wrapper{
position:relative;
}
.canvas_wrapper canvas{
position: absolute;
box-sizing: content-box;
font-kerning: normal !important;
}
.loaded .canvas_wrapper canvas{
border: 1px solid var(--border-color);
}
#mouse{
position:absolute;
pointer-events:none;
width:10px;
height:10px;
z-index:10;
}
#mouse.rect{
border:1px solid rgba(0,0,0,0.5);
}
#mouse.circle{
border:1px solid rgba(0,0,0,0.5);
border-radius:50%;
}
.transparent-grid{
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
/*background: url(images/icons/grid.png) repeat top left;*/
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAQElEQVQ4T2N89+7dfwYigKCgIBGqGBgYRw3EGU6jYYgzaIZAsvn//z9ROeX9+/fE5ZRRA3GG02gY4s4pgz7ZAAAnSWvHPkHXaAAAAABJRU5ErkJggg==') repeat top left;
z-index:1;
image-rendering: pixelated; /* disable antialiasing */
}
.transparent-grid.white{
background:white;
}
.transparent-grid.green{
background: #5be471;
}
.transparent-grid.grey{
background: #dfdfdf;
}
canvas{
position:relative;
z-index:2;
}
#canvas_back{
position: absolute;
background-color:#ffffff;
outline: none;
}
#canvas_grid{
pointer-events:none;
}
.group{
border:1px solid #999999;
margin: 5px 0px 5px 0px;
padding:5px 8px;
}
.flex-container{
display: flex;
flex-wrap: wrap;
}
.flex-container .item{
flex: auto;
margin: 2px 0;
width: 150px;
}
.flex-container .item:empty{
height: 0;
border: none;
}
.alertify-notifier{
color: black;
}
.effectsPreview{
cursor: pointer;
background-color: #ddd;
}
@media screen and (max-width:550px){
.canvas_wrapper{
margin-left: 0px;
}
}
@media screen and (max-height: 690px){
.sidebar_left{
width: 75px;
}
}
@media screen and (max-height:450px){
.sidebar_left{
width: 88px;
}
}
/* ========== dialogs ======================================================= */
#dialog_color_picker_group {
width: 60%;
}
#dialog_color_channel_group {
width: 40%;
margin-left: 1rem;
}
@media screen and (max-width: 450px) {
#dialog_color_picker .ui_flex_group {
flex-wrap: wrap;
}
#dialog_color_picker_group {
width: 100%;
}
#dialog_color_channel_group {
width: 100%;
margin-left: 0;
margin-top: 1rem;
}
}
+202
View File
@@ -0,0 +1,202 @@
:root {
--menu-dropdown-background-color: #ffffff;
--menu-dropdown-border-color: #49844d;
--menu-dropdown-text-color: #2d2b2b;
--menu-dropdown-text-muted-color: #aaaaaa;
--menu-dropdown-hover-background-color: #adecab;
--menu-dropdown-hover-text-color: #2d2d2d;
--menu-dropdown-divider-color: #e5e5e5;
}
.sr_only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.main_menu {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.main_menu > ul.menu_bar {
display: flex;
flex-direction: row;
list-style: none;
padding: 0;
margin: 0;
height: 30px;
padding-left: 10px;
background: var(--menu-background-color);
}
.main_menu > ul.menu_bar > li {
padding: 0;
overflow: hidden;
height: 100%;
}
.main_menu > ul.menu_bar > li > a {
display: flex;
align-items: center;
font-size: 12px;
color: var(--menu-text-color);
text-decoration: none;
padding: 0 10px;
height: 100%;
}
.main_menu > ul.menu_bar > li > a::-moz-focus-inner {
border: 0;
}
.main_menu > ul.menu_bar > li > a:focus {
outline: none;
box-shadow: 0 -3px var(--menu-dropdown-background-color) inset;
}
.main_menu > ul.menu_bar > li > a:hover {
background: var(--menu-dropdown-hover-background-color);
box-shadow: none;
color: var(--menu-dropdown-hover-text-color);
}
.main_menu > ul.menu_bar > li > a[aria-expanded="true"] {
background: var(--menu-dropdown-background-color);
box-shadow: none;
color: var(--menu-dropdown-text-color);
}
.main_menu > ul.menu_bar > li > a > * {
pointer-events: none;
}
.main_menu > ul.menu_dropdown {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
list-style: none;
padding: 0;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
min-width: 150px;
box-shadow: 0 0 0 1px var(--menu-dropdown-border-color);
background: var(--menu-dropdown-background-color);
}
.main_menu > ul.menu_dropdown > li {
padding: 0;
}
.main_menu > ul.menu_dropdown > li > hr {
background: none;
border: 1px solid var(--menu-dropdown-divider-color);
border-bottom: none;
margin: 0;
}
.main_menu > ul.menu_dropdown > li > a {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
height: 30px;
padding: 0 10px;
font-size: 12px;
line-height: 30px;
text-decoration: none;
color: var(--menu-dropdown-text-color);
}
.main_menu > ul.menu_dropdown > li > ::-moz-focus-inner {
border: 0;
}
.main_menu > ul.menu_dropdown > li > a:focus {
outline: none;
box-shadow: 0 0 0 2px var(--menu-dropdown-hover-background-color) inset;
}
.main_menu > ul.menu_dropdown > li > a:hover {
background: var(--menu-dropdown-hover-background-color);
box-shadow: none;
color: var(--menu-dropdown-hover-text-color);
}
.main_menu > ul.menu_dropdown > li > a[aria-expanded="true"] {
background: var(--menu-dropdown-hover-background-color);
box-shadow: none;
color: var(--menu-dropdown-hover-text-color);
}
.main_menu > ul.menu_dropdown > li > a[aria-haspopup="true"]::after {
position: absolute;
content: ">";
right: 9px;
width: 5px;
transform: scaleY(2);
color: #808080;
}
.main_menu > ul.menu_dropdown > li > a[aria-haspopup="true"] > .name {
margin-right: 8px;
}
.main_menu > ul.menu_dropdown > li > a[target="_blank"]::after {
content: "";
width: 10px;
height: 10px;
margin-left: 5px;
background: url('images/icons/external.png') no-repeat center center;
background-size: auto 8px;
opacity: 0.3;
}
.main_menu > ul.menu_dropdown > li > a > * {
pointer-events: none;
}
.main_menu > ul.menu_dropdown > li > a > .name {
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
}
.main_menu > ul.menu_dropdown > li > a > .shortcut {
flex-shrink: 1;
color: var(--menu-dropdown-text-muted-color);
}
.mobile_menu {
display: none;
position: absolute;
width: 100%;
top: 0;
}
.left_mobile_menu, .right_mobile_menu {
position: absolute;
width: 50px;
height: 50px;
display: block;
top: 0;
z-index: 200;
border: 0;
outline: 0;
cursor: pointer;
background-color: transparent;
}
.left_mobile_menu:after, .right_mobile_menu:after {
position: absolute;
content: '';
left:0;
top:0;
bottom:0;
right:0;
filter: var(--mobile-menu-toggle-filter);
background: url('images/icons/menu.svg') no-repeat center center;
background-size: auto 26px;
}
.left_mobile_menu { left:0; }
.right_mobile_menu { right:0; }
@media screen and (max-width:700px) {
.mobile_menu {
display: block;
}
.main_menu > ul.menu_bar {
height: 50px;
padding-left: 50px;
padding-right: 50px;
}
}
+271
View File
@@ -0,0 +1,271 @@
#popups:not(:empty) {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#popups .popup {
position:fixed;
display:none;
top: 15vh;
left: calc(100vw / 2);
transform: translate(-50%, 0);
background-color: #7A838B;
background-color: var(--block-background-color);
border: 1px solid rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
width: 90vw;
max-width: 500px;
max-height: calc(80vh);
margin:0px auto 0px auto;
padding: 4rem 0 5rem 0;
box-shadow: 0 0 0 4000px rgba(0,0,0,0.3), 0 0 20px rgba(0,0,0,0.5);
z-index: 100;
font-size: 13px;
overflow: hidden;
}
#popups .popup.wide{
max-width: 840px;
}
#popups .popup a{
color: var(--link-color);
}
#popups .popup h2{
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0;
height: 4rem;
line-height: 4rem;
padding: 0 1rem;
font-size: 1.8rem;
background-color: rgba(255, 255, 255, 0.3);
background-color: var(--header-background-color);
z-index: 0;
cursor:move;
}
#popups .popup .dialog_content {
overflow-y: auto;
max-height: calc(80vh - 11rem);
padding: 1rem;
}
#popups .popup .buttons{
position: absolute;
background-color: var(--block-background-color);
bottom: 0;
left: 0;
right: 0;
height: 5rem;
line-height: 4rem;
margin: 0;
padding: .5rem 0;
text-align: center;
border-top: 1px solid var(--header-background-color);
z-index: 3;
}
#popups .popup .close{
position: absolute;
right: 0;
top: 0;
min-width: 0;
padding: 5px;
line-height: 0.5;
font-size: 16px;
margin-top: 10px;
margin-right: 10px;
border: none;
background: none;
z-index: 1;
}
#popups .popup input[type="range"]{
margin:0;
width: 100%;
}
#popups .popup table{
box-sizing: border-box;
width: 100%;
}
#popups .popup td, #popups .popup th{
height: 25px;
}
#popups .popup td{
vertical-align: middle;
}
#popups .popup th{
text-align:left;
padding: 5px 5px 5px 0;
width: 130px;
}
#popups .popup textarea{
color: var(--input-text-color);
width:100%;
border:1px solid #393939;
padding-left:5px;
}
#popups .popup .button{
margin: 0 3px;
background-color: rgba(255, 255, 255, 0.2);
background-color: var(--button-background-color);
min-width:60px;
border:1px solid rgba(0, 0, 0, 0.5);
border:1px solid var(--border-color);
padding: 5px 10px;
}
#popups .popup input[type="text"], #popups .popup input[type="number"], #popups .popup textarea{
width:100%;
}
#popups .popup input[type="number"]{
width:100px;
}
#popups .popup input[type="radio"], #popups .popup input[type="checkbox"]{
margin-left: 0;
}
#popups .popup label span{
color:var(--text-color-muted);
}
#popups .popup .checkbox label{
margin-top: 5px;
color:var(--text-color-muted);
}
#popups .popup .preview_container{
margin-top:10px;
margin-bottom:15px;
text-align: center;
}
#popups .popup .preview_canvas_left{
position:relative;
margin:0 5px 5px 0;
border:1px solid #393939;
display: inline-block;
vertical-align: top;
}
#popups .popup .preview_canvas_post_back{
position:absolute;
border:1px solid #393939;
background-color:#ffffff;
}
#popups .popup .preview_canvas_post{
position:relative;
border:1px solid #393939;
}
#popups .popup .canvas_preview_container{
position:relative;
display: inline-block;
vertical-align: top;
}
#popups .popup .radios label{
display: inline-block;
margin-right: 10px;
}
#popups .popup .range_value{
padding-left:10px;
width:50px;
}
#popups .popup .long_text_value{
font-size: 12px;
}
#popups .popup .preview-item-title{
text-align: center;
max-width: 150px;
}
#popups .popup .field_comment{
display: inline-block;
margin-left: 10px;
opacity: 0.5;
}
#popups .popup .selection_card {
background: var(--input-background-color);
display: block;
width: 100%;
padding: 0;
border-bottom: 0.1rem solid var(--input-border-color);
overflow: hidden;
position: relative;
}
#popups .popup .selection_card:first-child {
margin-top: 1rem;
border-radius: var(--input-border-radius) var(--input-border-radius) 0 0;
}
#popups .popup .selection_card:last-child {
border-radius: 0 0 var(--input-border-radius) var(--input-border-radius);
border-bottom: none;
}
#popups .popup .selection_card > input[type="checkbox"] {
flex-grow: 0;
flex-shrink: 0;
margin: 0;
cursor: pointer;
position: absolute;
top: 50%;
left: 1.5rem;
transform: translateY(-50%) scale(1.5);
}
#popups .popup .selection_card > input[type="checkbox"] + label {
display: block;
width: 100%;
flex-grow: 1;
flex-shrink: 1;
margin: 0;
padding: 1rem 0.5rem 1rem 5.5rem;
cursor: pointer;
}
#popups .popup .selection_card > input[type="checkbox"] + label:hover {
background: var(--input-background-color-hover);
}
#popups .popup .selection_card .font_preview {
font-size: 1.6rem;
height: 2.5rem;
line-height: 2.5rem;
white-space: nowrap;
}
#popups .popup .pagination {
display: flex;
text-align: center;
margin: 1rem 0 0 0;
}
#popups .popup .pagination button {
flex-grow: 0;
height: 2.8rem;
line-height: 2.8rem;
border-radius: 0;
margin-left: -1px;
min-width: 3.3rem;
}
#popups .popup .pagination button:first-child {
border-radius: var(--button-border-radius) 0 0 var(--button-border-radius);
margin-left: auto;
}
#popups .popup .pagination button:last-child {
border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
margin-right: auto;
}
@media screen and (max-width:500px){
#popups .popup {
max-height: calc(80vh - 20px); /* mobile phones has bottom menu */
}
#popups .popup tr{
display: block;
margin-bottom: 10px;
}
#popups .popup td, #popups .popup th{
display: block;
width: 100%;
height: auto;
padding: 5px;
}
#popups .popup th{
padding: 5px 5px 0px 5px;
}
#popups .popup td{
padding: 5px 5px 5px 5px;
}
#popups .popup .range_value{
display: none;
}
}
+34
View File
@@ -0,0 +1,34 @@
@media print{
body{
background:none !important;
background: #fff;
background-color: #fff;
font-family: Arial,Helvetica,Verdana;
width:auto !important;
padding:5px !important;
font-size: 12px;
}
progress,
.menu,
.sidebar_left,
.sidebar_right,
.submenu,
.main_menu{
display: none;
height: 0;
width: 0;
}
.main_wrapper{
margin:0px;
padding:0px;
}
canvas{
border:0px;
position: absolute;
top:0px;
left:0px;
}
.canvas_wrapper canvas{
border:0;
}
}
+230
View File
@@ -0,0 +1,230 @@
:root {
/* original - default */
--background: #666d6f;
--text-color: #f4f3f3;
--text-color-muted: #c1c1c1;
--text-color-red: #e38282;
--text-color-green: #8bdb8b;
--text-color-blue: #a4a4ff;
--link-color: #9ffda5;
--section-background-color: #323a3c;
--area-background-color: #464d4f;
--block-background-color: #464d4f;
--header-background-color: #373d3f;
--button-background-color: #2f3739;
--button-background-color-hover: #75df72;
--button-background-color-active: #4d5153;
--button-shadow-color: rgba(0, 0, 0, 0.3);
--button-text-color-active: #adecab;
--button-border-radius: .4rem;
--button-toggle-background-color: #575f62;
--button-toggle-background-color-hover: #575f62;
--input-background-color: #2f3739;
--input-background-color-hover: #383f44;
--input-text-color: #f4f3f3;
--input-border-color: #0f0f0f;
--input-border-color-active: #70996e;
--input-border-radius: .4rem;
--input-group-border-color: #323a3c;
--menu-background-color: #222;
--menu-icons-filter: invert(1);
--menu-icons-filter-active: none;
--menu-text-color: #cccccc;
--number-input-arrow-width: 2rem;
--background-color-active: #adecab;
--background-color-hover: #575f62;
--text-color-active: #215b2a;
--border-color: #727677;
--scrollbar-track-color: #464d4f;
--scrollbar-thumb-color: #2f3739;
--mobile-menu-toggle-filter: invert(1);
}
body.theme-light{
/* light */
--background: #f9f9fa;
--text-color: #0c0c0d;
--text-color-muted: #444444;
--text-color-red: #bb2424;
--text-color-green: #2b882b;
--text-color-blue: #5454ca;
--link-color: #000080;
--section-background-color: #eaeaea;
--area-background-color: #d9d9d9;
--block-background-color: #eaeaea;
--header-background-color: #dbdbdb;
--button-background-color: #f9f9fa;
--button-background-color-hover: #ddd;
--button-background-color-active: #f3f3f3;
--button-text-color-active: #59aed8;
--button-shadow-color: rgba(0, 0, 0, 0.1);
--button-toggle-background-color: #b7b7b7;
--button-toggle-background-color-hover: #b7b7b7;
--input-background-color: #ffffff;
--input-background-color-hover: #ddd;
--input-text-color: #0c0c0d;
--input-border-color: #ccc;
--input-border-color-active: #59aed8;
--input-group-border-color: #c4c4c4;
--menu-background-color: #eaeaea;
--menu-icons-filter: none;
--menu-icons-filter-active: invert(1);
--menu-text-color: #333333;
--menu-dropdown-hover-background-color: #a3dbf7;
--menu-dropdown-border-color: #15439b;
--background-color-active: #a3dbf7;
--background-color-hover: #c4c4c4;
--text-color-active: #15439b;
--border-color: #c1c1c1;
--scrollbar-track-color: #f9f9fa;
--scrollbar-thumb-color: #919090;
--mobile-menu-toggle-filter: none;
}
body.theme-green{
/* green */
--background: #050702;
--text-color: #acc3a9;
--text-color-muted: #80937d;
--link-color: #9ffda5;
--section-background-color: #1c2e04;
--area-background-color: #3b5f11;
--block-background-color: #3b5f11;
--header-background-color: #2b460f;
--button-background-color: #2e4a0d;
--button-background-color-hover: #58960e;
--button-background-color-active:#2b460f;
--button-text-color-active: #ccc;
--button-toggle-background-color: #243e05;
--button-toggle-background-color-hover: #243e05;
--input-background-color: #ffffff;
--input-background-color-hover: #ddd;
--input-text-color: #0c0c0d;
--input-border-color: #ccc;
--menu-background-color: #1c2e04;
--menu-icons-filter: invert(1);
--menu-icons-filter-active: none;
--menu-text-color: #acc3a9;
--background-color-active: #58960e;
--background-color-hover: #58960e;
--text-color-active: #acc3a9;
--border-color: #4d6b1e;
--scrollbar-track-color: #050702;
--scrollbar-thumb-color: #80937d;
--mobile-menu-toggle-filter: invert(1);
}
*{
box-sizing: border-box;
background-repeat: no-repeat;
}
html {
font-size: 10px; /* Base is 10px for easy REM calculation */
}
body{
margin: 0;
padding: 30px 0 0 0;
background-color: #424F5A;
background: var(--background);
font-size: 1.3rem;
font-family: Arial, Helvetica, sans-serif;
color: var(--text-color);
line-height: 1.4;
font-weight: normal;
overflow: hidden;
}
canvas{
outline: none;
/* disable select canvas */
-webkit-touch-callout: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
img{
border: none;
}
td, th{
vertical-align:top;
}
table{
border: 0;
margin: 0;
padding: 0;
vertical-align: baseline;
border-collapse: collapse;
border-spacing: 0;
width:100%;
}
hr{
border-color: rgba(0,0,0,0.3);
border-bottom: 0;
}
input[type="text"], select, input[type="number"], textarea{
background: var(--input-background-color);
border: 1px solid var(--input-border-color);
border-radius: var(--input-border-radius);
color: var(--input-text-color);
padding: 3px 5px;
font-size: 13px;
}
input:disabled {
opacity: 0.3;
}
select{
padding: 2px 4px;
}
input[type="range"]{
margin-left: 0;
width:100%;
}
button, input[type="button"]{
border-radius: var(--button-border-radius);
box-shadow: 0 1px 2px 0 var(--button-shadow-color), 0 1px 0 0 rgba(255, 255, 255, 0.1) inset;
cursor: pointer;
border: 1px solid var(--border-color);
background-color: var(--button-background-color);
color: var(--text-color);
}
button:hover, input[type="button"]:hover{
background-color: var(--button-background-color-hover);
}
button:disabled, input[type="button"]:disabled{
visibility:hidden;
}
button[aria-pressed="true"], input[type="button"][aria-pressed="true"]{
background-color: var(--button-background-color-active);
color: var(--button-text-color-active);
box-shadow: 0 1px 2px 0 var(--button-shadow-color), 0 1px 1px 1.5px rgba(58, 40, 40, 0.1) inset, 0 -1px 0 0 var(--button-text-color-active) inset;
}
button[aria-pressed="true"]:hover, input[type="button"][aria-pressed="true"]:hover{
background-color: var(--button-background-color-hover);
}
button.ui_toggle_button:hover{
background-color: var(--button-toggle-background-color-hover);
}
label{
display: inline-block;
vertical-align: top;
margin-top: 7px;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track-piece {
background: rgba(0,0,0,0.3);
}
::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.6);
}
@supports (zoom:2) {
input[type="radio"], input[type=checkbox]{
zoom: 1.5;
}
}
@supports not (zoom:2) {
input[type="radio"], input[type=checkbox]{
transform: scale(1.5);
transform-origin: left center;
margin: 8px 12px 8px 0;
}
}
+81
View File
@@ -0,0 +1,81 @@
/* Common input label sizes */
.label_width_character {
width: 100%;
max-width: 2.88rem;
overflow: hidden;
flex-shrink: 0;
}
.label_width_small {
width: 100%;
max-width: 6.4rem;
overflow: hidden;
}
.label_width_medium {
width: 100%;
max-width: 10.4rem;
overflow: hidden;
}
/* Font color utility */
.text_red { color: var(--text-color-red); }
.text_green { color: var(--text-color-green); }
.text_blue { color: var(--text-color-blue); }
.text_muted { color: var(--text-color-muted); }
/*
Size inputs based on the number of "w" characters that could fit in the input. "w" is usually the widest character.
This is a rough estimate since all characters vary in width. For example an input with numbers
usually fits way more characters than an input with letters.
"cw" means character width
*/
.input_cw_1, .input_cw_2, .input_cw_3, .input_cw_4, .input_cw_5,
.input_cw_6, .input_cw_7, .input_cw_8, .input_cw_9, .input_cw_10
.input_cw_11, .input_cw_12, .input_cw_13, .input_cw_14, .input_cw_15 {
width: 100%;
}
.input_cw_1 { max-width: 2.25rem; }
.input_cw_2 { max-width: 3.25rem; }
.input_cw_3 { max-width: 4.25rem; }
.input_cw_4 { max-width: 5.25rem; }
.input_cw_5 { max-width: 6.25rem; }
.input_cw_6 { max-width: 7.25rem; }
.input_cw_7 { max-width: 8.25rem; }
.input_cw_8 { max-width: 9.25rem; }
.input_cw_9 { max-width: 10.25rem; }
.input_cw_10 { max-width: 11.25rem; }
.input_cw_11 { max-width: 12.25rem; }
.input_cw_12 { max-width: 13.25rem; }
.input_cw_13 { max-width: 14.25rem; }
.input_cw_14 { max-width: 15.25rem; }
.input_cw_15 { max-width: 16.25rem; }
input[type="number"].input_cw_1 { max-width: 4.25rem; }
input[type="number"].input_cw_2 { max-width: 5.25rem; }
input[type="number"].input_cw_3 { max-width: 6.25rem; }
input[type="number"].input_cw_4 { max-width: 7.25rem; }
input[type="number"].input_cw_5 { max-width: 8.25rem; }
input[type="number"].input_cw_6 { max-width: 9.25rem; }
input[type="number"].input_cw_7 { max-width: 10.25rem; }
input[type="number"].input_cw_8 { max-width: 11.25rem; }
input[type="number"].input_cw_9 { max-width: 12.25rem; }
input[type="number"].input_cw_10 { max-width: 13.25rem; }
input[type="number"].input_cw_11 { max-width: 14.25rem; }
input[type="number"].input_cw_12 { max-width: 15.25rem; }
input[type="number"].input_cw_13 { max-width: 16.25rem; }
input[type="number"].input_cw_14 { max-width: 17.25rem; }
input[type="number"].input_cw_15 { max-width: 18.25rem; }
.ui_number_input.input_cw_1 { max-width: calc(2.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_2 { max-width: calc(3.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_3 { max-width: calc(4.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_4 { max-width: calc(5.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_5 { max-width: calc(6.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_6 { max-width: calc(7.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_7 { max-width: calc(8.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_8 { max-width: calc(9.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_9 { max-width: calc(10.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_10 { max-width: calc(11.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_11 { max-width: calc(12.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_12 { max-width: calc(13.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_13 { max-width: calc(14.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_14 { max-width: calc(15.25rem + var(--number-input-arrow-width)); }
.ui_number_input.input_cw_15 { max-width: calc(16.25rem + var(--number-input-arrow-width)); }
-71
View File
@@ -1,71 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #1a1a1a;
color: #ffffff;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
button {
cursor: pointer;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
input[type="text"],
input[type="file"],
textarea {
padding: 8px;
border: 1px solid #444;
border-radius: 4px;
background-color: #2a2a2a;
color: #ffffff;
font-size: 14px;
}
input[type="range"] {
width: 100%;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
margin-bottom: 30px;
border-bottom: 2px solid #333;
padding-bottom: 20px;
}
.header h1 {
font-size: 32px;
margin-bottom: 8px;
}
.header p {
color: #888;
font-size: 14px;
}
+5
View File
@@ -0,0 +1,5 @@
# Managing Undo History with Actions
More information on wiki page:
https://github.com/viliusle/miniPaint/wiki/Undo-Redo-system
+145
View File
@@ -0,0 +1,145 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
export class Activate_tool_action extends Base_action {
/**
* Groups multiple actions together in the undo/redo history, runs them all at once.
*/
constructor(key, ignore_same_tool) {
super('activate_tool', 'Activate Tool');
this.ignore_same_tool = !!ignore_same_tool;
this.key = key;
this.old_key = null;
this.tool_leave_actions = null;
this.tool_activate_actions = null;
}
async do() {
super.do();
const key = this.key;
this.old_key = app.GUI.GUI_tools.active_tool;
if (this.key !== this.old_key || this.ignore_same_tool) {
//reset last
document.querySelector('#tools_container .' + this.old_key).classList.remove("active");
//send exit event to old previous tool
if (config.TOOL.on_leave != undefined) {
var moduleKey = config.TOOL.name;
var functionName = config.TOOL.on_leave;
this.tool_leave_actions = app.GUI.GUI_tools.tools_modules[moduleKey].object[functionName]();
if (this.tool_leave_actions) {
for (let action of this.tool_leave_actions) {
await action.do();
}
}
}
//change active
app.GUI.GUI_tools.active_tool = key;
document.querySelector('#tools_container .' + app.GUI.GUI_tools.active_tool)
.classList.add("active");
for (let i in config.TOOLS) {
if (config.TOOLS[i].name == app.GUI.GUI_tools.active_tool) {
config.TOOL = config.TOOLS[i];
}
}
//check module
if (app.GUI.GUI_tools.tools_modules[key] == undefined) {
alertify.error('Tools class not found: ' + key);
return;
}
//set default cursor
const mainWrapper = document.getElementById('main_wrapper');
const defaultCursor = config.TOOL && config.TOOL.name === 'text' ? 'text' : 'default';
if (mainWrapper.style.cursor != defaultCursor) {
mainWrapper.style.cursor = defaultCursor;
}
app.GUI.GUI_tools.show_action_attributes();
app.GUI.GUI_tools.Helper.setCookie('active_tool', app.GUI.GUI_tools.active_tool);
}
//send activate event to new tool
if (config.TOOL.on_activate != undefined) {
var moduleKey = config.TOOL.name;
var functionName = config.TOOL.on_activate;
this.tool_activate_actions = app.GUI.GUI_tools.tools_modules[moduleKey].object[functionName]();
if (this.tool_activate_actions) {
for (let action of this.tool_activate_actions) {
await action.do();
}
}
}
config.need_render = true;
}
async undo() {
super.undo();
// Undo activate actions
if (this.tool_activate_actions) {
for (let action of this.tool_activate_actions) {
await action.undo();
action.free();
}
this.tool_activate_actions = null;
}
//reset last
document.querySelector('#tools_container .' + this.key)
.classList.remove("active");
//change active
app.GUI.GUI_tools.active_tool = this.old_key;
document.querySelector('#tools_container .' + app.GUI.GUI_tools.active_tool)
.classList.add("active");
for (let i in config.TOOLS) {
if (config.TOOLS[i].name == app.GUI.GUI_tools.active_tool) {
config.TOOL = config.TOOLS[i];
}
}
app.GUI.GUI_tools.show_action_attributes();
app.GUI.GUI_tools.Helper.setCookie('active_tool', app.GUI.GUI_tools.active_tool);
//set default cursor
const mainWrapper = document.getElementById('main_wrapper');
const defaultCursor = config.TOOL && config.TOOL.name === 'text' ? 'text' : 'default';
if (mainWrapper.style.cursor != defaultCursor) {
mainWrapper.style.cursor = defaultCursor;
}
// Undo leave actions
if (this.tool_leave_actions) {
for (let action of this.tool_leave_actions) {
await action.undo();
action.free();
}
this.tool_leave_actions = null;
}
config.need_render = true;
}
free() {
if (this.tool_activate_actions) {
for (let action of this.tool_activate_actions) {
action.free();
}
this.tool_activate_actions = null;
}
if (this.tool_leave_actions) {
for (let action of this.tool_leave_actions) {
action.free();
}
this.tool_leave_actions = null;
}
}
}
@@ -0,0 +1,67 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
export class Add_layer_filter_action extends Base_action {
/**
* register new live filter
*
* @param {int} layer_id
* @param {string} name
* @param {object} params
*/
constructor(layer_id, name, params, filter_id) {
super('add_layer_filter', 'Add Layer Filter');
if (layer_id == null)
layer_id = config.layer.id;
this.layer_id = parseInt(layer_id);
this.name = name;
this.params = params;
this.filter_id = filter_id;
this.reference_layer = null;
}
async do() {
super.do();
this.reference_layer = app.Layers.get_layer(this.layer_id);
if (!this.reference_layer) {
throw new Error('Aborted - layer with specified id doesn\'t exist');
}
var filter = {
id: this.filter_id,
name: this.name,
params: this.params,
};
if(this.filter_id) {
//update
for(var i in this.reference_layer.filters) {
if(this.reference_layer.filters[i].id == this.filter_id){
this.reference_layer.filters[i] = filter;
break;
}
}
}
else{
//insert
filter.id = Math.floor(Math.random() * 999999999) + 1; // A good UUID library would
this.reference_layer.filters.push(filter);
}
config.need_render = true;
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.reference_layer) {
this.reference_layer.filters.pop();
this.reference_layer = null;
}
config.need_render = true;
app.GUI.GUI_layers.render_layers();
}
free() {
this.reference_layer = null;
this.params = null;
}
}
@@ -0,0 +1,100 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
import Tools_settings_class from './../modules/tools/settings.js';
export class Autoresize_canvas_action extends Base_action {
/**
* autoresize canvas to layer size, based on dimensions, up - always, if 1 layer - down.
*
* @param {int} width
* @param {int} height
* @param {int} layer_id
* @param {boolean} can_automate
*/
constructor(width, height, layer_id, can_automate = true, ignore_same_size = false) {
super('autoresize_canvas', 'Auto-resize Canvas');
this.Tools_settings = new Tools_settings_class();
this.width = width;
this.height = height;
this.layer_id = layer_id;
this.can_automate = can_automate;
this.ignore_same_size = ignore_same_size;
this.old_config_width = null;
this.old_config_height = null;
}
async do() {
super.do();
const width = this.width;
const height = this.height;
const can_automate = this.can_automate;
let need_fit = false;
let new_config_width = config.WIDTH;
let new_config_height = config.HEIGHT;
var enable_autoresize = this.Tools_settings.get_setting('enable_autoresize');
if(enable_autoresize == false){
return;
}
// Resize up
if (width > new_config_width || height > new_config_height) {
const wrapper = document.getElementById('main_wrapper');
const page_w = wrapper.clientWidth;
const page_h = wrapper.clientHeight;
if (width > page_w || height > page_h) {
need_fit = true;
}
if (width > new_config_width)
new_config_width = parseInt(width);
if (height > new_config_height)
new_config_height = parseInt(height);
}
// Resize down
if (config.layers.length == 1 && can_automate !== false) {
if (width < new_config_width)
new_config_width = parseInt(width);
if (height < new_config_height)
new_config_height = parseInt(height);
}
if (new_config_width !== config.WIDTH || new_config_height !== height) {
this.old_config_width = config.WIDTH;
this.old_config_height = config.HEIGHT;
config.WIDTH = new_config_width;
config.HEIGHT = new_config_height;
app.GUI.prepare_canvas();
} else if (!this.ignore_same_size) {
throw new Error('Aborted - Resize not necessary')
}
// Fit zoom when after short pause
// @todo - remove setTimeout
if (need_fit == true) {
await new Promise((resolve) => {
window.setTimeout(() => {
app.GUI.GUI_preview.zoom_auto();
resolve();
}, 100);
});
}
}
async undo() {
super.undo();
if (this.old_config_width != null) {
config.WIDTH = this.old_config_width;
}
if (this.old_config_height != null) {
config.HEIGHT = this.old_config_height;
}
if (this.old_config_width != null || this.old_config_height != null) {
app.GUI.prepare_canvas();
}
this.old_config_width = null;
this.old_config_height = null;
}
}
+19
View File
@@ -0,0 +1,19 @@
export class Base_action {
constructor(action_id, action_description) {
this.action_id = action_id;
this.action_description = action_description;
this.is_done = false;
this.memory_estimate = 0; // Estimate of how much memory will be freed when the free() method is called (in bytes)
this.database_estimate = 0; // Estimate of how much database space will be freed when the free() method is called (in bytes)
}
do() {
this.is_done = true;
}
undo() {
this.is_done = false;
}
free() {
// Override if need to run tasks to free memory when action is discarded from history
}
}
+59
View File
@@ -0,0 +1,59 @@
import config from '../config.js';
import { Base_action } from './base.js';
export class Bundle_action extends Base_action {
/**
* Groups multiple actions together in the undo/redo history, runs them all at once.
*/
constructor(bundle_id, bundle_name, actions_to_do) {
super(bundle_id, bundle_name);
this.actions_to_do = actions_to_do;
}
async do() {
super.do();
let error = null;
let i = 0;
this.memory_estimate = 0;
this.database_estimate = 0;
for (i = 0; i < this.actions_to_do.length; i++) {
try {
await this.actions_to_do[i].do();
this.memory_estimate += this.actions_to_do[i].memory_estimate;
this.database_estimate += this.actions_to_do[i].database_estimate;
} catch (e) {
error = e;
break;
}
}
// One of the actions aborted, undo all previous actions.
if (error) {
for (i--; i >= 0; i--) {
await this.actions_to_do[i].undo();
}
throw error;
}
config.need_render = true;
}
async undo() {
super.undo();
this.memory_estimate = 0;
this.database_estimate = 0;
for (let i = this.actions_to_do.length - 1; i >= 0; i--) {
await this.actions_to_do[i].undo();
this.memory_estimate += this.actions_to_do[i].memory_estimate;
this.database_estimate += this.actions_to_do[i].database_estimate;
}
config.need_render = true;
}
free() {
if (this.actions_to_do) {
for (let action of this.actions_to_do) {
action.free();
}
this.actions_to_do = null;
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
export class Clear_layer_action extends Base_action {
/**
* clear layer data
*
* @param {int} layer_id
*/
constructor(layer_id) {
super('clear_layer', 'Clear Layer');
this.layer_id = parseInt(layer_id);
this.update_layer_action = null;
this.delete_layer_settings_action = null;
}
async do() {
super.do();
let layer = app.Layers.get_layer(this.layer_id);
if (!layer) {
throw new Error('Aborted - layer with specified id doesn\'t exist');
}
let new_settings = {
x: 0,
y: 0,
width: 0,
height: 0,
visible: true,
opacity: 100,
composition: null,
rotate: 0,
data: null,
params: {},
status: null,
render_function: null,
type: null
};
if (layer.type == 'image') {
//clean image
new_settings.link = null;
}
this.update_layer_action = new app.Actions.Update_layer_action(this.layer_id, new_settings);
await this.update_layer_action.do();
let delete_setting_names = [];
for (let prop_name in layer) {
//remove private attributes
if (prop_name[0] == '_') {
delete_setting_names.push(prop_name);
}
}
if (delete_setting_names.length > 0) {
this.delete_layer_settings_action = new app.Actions.Delete_layer_settings_action(this.layer_id, delete_setting_names);
await this.delete_layer_settings_action.do();
}
}
async undo() {
super.undo();
if (this.delete_layer_settings_action) {
await this.delete_layer_settings_action.undo();
this.delete_layer_settings_action.free();
this.delete_layer_settings_action = null;
}
if (this.update_layer_action) {
await this.update_layer_action.undo();
this.update_layer_action.free();
this.update_layer_action = null;
}
}
free() {
if (this.update_layer_action) {
this.update_layer_action.free();
this.update_layer_action = null;
}
if (this.delete_layer_settings_action) {
this.delete_layer_settings_action.free();
this.delete_layer_settings_action = null;
}
}
}
@@ -0,0 +1,60 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
export class Delete_layer_filter_action extends Base_action {
/**
* delete live filter
*
* @param {int} layer_id
* @param {string} filter_id
*/
constructor(layer_id, filter_id) {
super('delete_layer_filter', 'Delete Layer Filter');
if (layer_id == null)
layer_id = config.layer.id;
this.layer_id = parseInt(layer_id);
this.filter_id = filter_id;
this.reference_layer = null;
this.filter_remove_index = null;
this.old_filter = null;
}
async do() {
super.do();
this.reference_layer = app.Layers.get_layer(this.layer_id);
if (!this.reference_layer) {
throw new Error('Aborted - layer with specified id doesn\'t exist');
}
this.old_filter = null;
for (let i in this.reference_layer.filters) {
if (this.reference_layer.filters[i].id == this.filter_id) {
this.filter_remove_index = i;
this.old_filter = this.reference_layer.filters.splice(i, 1)[0];
break;
}
}
if (!this.old_filter) {
throw new Error('Aborted - filter with specified id doesn\'t exist in layer');
}
config.need_render = true;
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.reference_layer && this.old_filter) {
this.reference_layer.filters.splice(this.filter_remove_index, 0, this.old_filter);
}
this.reference_layer = null;
this.old_filter = null;
this.filter_remove_index = null;
config.need_render = true;
app.GUI.GUI_layers.render_layers();
}
free() {
this.reference_layer = null;
this.old_filter = null;
}
}
@@ -0,0 +1,50 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
export class Delete_layer_settings_action extends Base_action {
/**
* Deletes the specified settings in a layer
*
* @param {int} layer_id
* @param {array} setting_names
*/
constructor(layer_id, setting_names) {
super('delete_layer_settings', 'Delete Layer Settings');
this.layer_id = parseInt(layer_id);
this.setting_names = setting_names;
this.reference_layer = null;
this.old_settings = {};
}
async do() {
super.do();
this.reference_layer = app.Layers.get_layer(this.layer_id);
if (!this.reference_layer) {
throw new Error('Aborted - layer with specified id doesn\'t exist');
}
for (let name in this.setting_names) {
this.old_settings[name] = this.reference_layer[name];
delete this.reference_layer[name];
}
config.need_render = true;
}
async undo() {
super.undo();
if (this.reference_layer) {
for (let i in this.old_settings) {
this.reference_layer[i] = this.old_settings[i];
}
this.old_settings = {};
}
this.reference_layer = null;
config.need_render = true;
}
free() {
this.setting_names = null;
this.reference_layer = null;
this.old_settings = null;
}
}
+115
View File
@@ -0,0 +1,115 @@
import config from '../config.js';
import app from './../app.js';
import { Base_action } from './base.js';
export class Delete_layer_action extends Base_action {
/**
* removes layer
*
* @param {int} id
* @param {boolean} force - Force to delete first layer?
*/
constructor(layer_id, force) {
super('delete_layer', 'Delete Layer');
this.layer_id = parseInt(layer_id);
this.force = force || false;
this.insert_layer_action = null;
this.select_layer_action = null;
this.delete_index = null;
this.deleted_layer = null;
}
async do() {
super.do();
const id = this.layer_id;
const force = this.force;
// Determine if there is a layer to delete, abort if not
for (var i in config.layers) {
if (config.layers[i].id == id) {
this.delete_index = i;
}
}
if (this.delete_index === null) {
throw new Error('Aborted - Layer to delete not found');
}
if (config.layers.length == 1 && (force == undefined || force == false)) {
// Only 1 layer left
if (config.layer.type == null) {
//STOP
throw new Error('Aborted - Will not delete last layer');
}
else {
// Delete it, but before that - create new empty layer
this.insert_layer_action = new app.Actions.Insert_layer_action();
this.insert_layer_action.do();
}
}
if (config.layers.length > 1 && config.layer.id == id) {
// Select next or previous layer
try {
const select_action = new app.Actions.Select_next_layer_action(id);
await select_action.do();
this.select_layer_action = select_action;
} catch (error) {
const select_action = new app.Actions.Select_previous_layer_action(id);
await select_action.do();
this.select_layer_action = select_action;
}
}
// Remove layer from list
this.deleted_layer = config.layers.splice(this.delete_index, 1)[0];
// Estimate memory
if (this.deleted_layer.link && this.deleted_layer.link.src && typeof this.deleted_layer.link.src === 'string') {
this.memory_estimate = new Blob([this.deleted_layer.link.src]).size;
}
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.deleted_layer) {
config.layers.splice(this.delete_index, 0, this.deleted_layer);
this.delete_index = null;
this.deleted_layer = null;
}
if (this.select_layer_action) {
await this.select_layer_action.undo();
this.select_layer_action.free();
this.select_layer_action = null;
}
if (this.insert_layer_action) {
await this.insert_layer_action.undo();
this.insert_layer_action.free();
this.insert_layer_action = null;
}
// Estimate memory
this.memory_estimate = 0;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
free() {
if (this.deleted_layer) {
delete this.deleted_layer.link;
delete this.deleted_layer.data;
}
if (this.insert_layer_action) {
this.insert_layer_action.free();
this.insert_layer_action = null;
}
if (this.select_layer_action) {
this.select_layer_action.free();
this.select_layer_action = null;
}
this.deleted_layer = null;
}
}
+26
View File
@@ -0,0 +1,26 @@
export { Activate_tool_action } from './activate-tool.js';
export { Add_layer_filter_action } from './add-layer-filter.js';
export { Autoresize_canvas_action } from './autoresize-canvas.js';
export { Bundle_action } from './bundle.js';
export { Clear_layer_action } from './clear-layer.js';
export { Delete_layer_action } from './delete-layer.js';
export { Delete_layer_filter_action } from './delete-layer-filter.js';
export { Delete_layer_settings_action } from './delete-layer-settings.js';
export { Init_canvas_zoom_action } from './init-canvas-zoom.js';
export { Insert_layer_action } from './insert-layer.js';
export { Prepare_canvas_action } from './prepare-canvas.js';
export { Reorder_layer_action } from './reorder-layer.js';
export { Reset_layers_action } from './reset-layers.js';
export { Refresh_action_attributes_action } from './refresh-action-attributes.js';
export { Refresh_layers_gui_action } from './refresh-layers-gui.js';
export { Reset_selection_action } from './reset-selection.js';
export { Select_layer_action } from './select-layer.js';
export { Select_next_layer_action } from './select-next-layer.js';
export { Select_previous_layer_action } from './select-previous-layer.js';
export { Set_object_property_action } from './set-object-property.js';
export { Set_selection_action } from './set-selection.js';
export { Stop_animation_action } from './stop-animation.js';
export { Toggle_layer_visibility_action } from './toggle-layer-visibility.js';
export { Update_config_action } from './update-config.js';
export { Update_layer_image_action } from './update-layer-image.js';
export { Update_layer_action } from './update-layer.js';
@@ -0,0 +1,45 @@
import app from '../app.js';
import config from '../config.js';
import zoomView from '../libs/zoomView.js';
import { Base_action } from './base.js';
export class Init_canvas_zoom_action extends Base_action {
/**
* Resets the canvas
*/
constructor() {
super('init_canvas_zoom', 'Initialize Canvas Zoom');
this.old_bounds = null;
this.old_context = null;
this.old_stable_dimensions = null;
}
async do() {
super.do();
this.old_bounds = zoomView.getBounds();
this.old_context = zoomView.getContext();
this.old_stable_dimensions = app.Layers.stable_dimensions;
zoomView.setBounds(0, 0, config.WIDTH, config.HEIGHT);
zoomView.setContext(app.Layers.ctx);
app.Layers.stable_dimensions = [
config.WIDTH,
config.HEIGHT
];
}
async undo() {
super.undo();
zoomView.setBounds(this.old_bounds.top, this.old_bounds.left, this.old_bounds.right, this.old_bounds.bottom);
zoomView.setContext(this.old_context);
app.Layers.stable_dimensions = this.old_stable_dimensions;
this.old_bounds = null;
this.old_context = null;
this.old_stable_dimensions = null;
}
free() {
this.old_bounds = null;
this.old_context = null;
this.old_stable_dimensions = null;
}
}
+214
View File
@@ -0,0 +1,214 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
export class Insert_layer_action extends Base_action {
/**
* Creates new layer
*
* @param {object} settings
* @param {boolean} can_automate
*/
constructor(settings, can_automate = true) {
super('insert_layer', 'Insert Layer');
this.settings = settings;
this.can_automate = can_automate;
this.previous_auto_increment = null;
this.previous_selected_layer = null;
this.inserted_layer_id = null;
this.update_layer_action = null;
this.delete_layer_action = null;
this.autoresize_canvas_action = null;
}
async do() {
super.do();
this.previous_auto_increment = app.Layers.auto_increment;
this.previous_selected_layer = config.layer;
let autoresize_as = null;
// Default data
const layer = {
id: app.Layers.auto_increment,
parent_id: 0,
name: config.TOOL.name.charAt(0).toUpperCase() + config.TOOL.name.slice(1) + ' #' + app.Layers.auto_increment,
type: null,
link: null,
x: 0,
y: 0,
width: null,
width_original: null,
height: null,
height_original: null,
visible: true,
is_vector: false,
hide_selection_if_active: false,
opacity: 100,
order: app.Layers.auto_increment,
composition: 'source-over',
rotate: 0,
data: null,
params: {},
status: null,
color: config.COLOR,
filters: [],
render_function: null,
};
// Build data
for (let i in this.settings) {
if (typeof layer[i] == "undefined" && !i.startsWith('_')) {
alertify.error('Error: wrong key: ' + i);
continue;
}
layer[i] = this.settings[i];
}
// Prepare image
let image_load_promise;
if (layer.type == 'image') {
if(layer.name.toLowerCase().indexOf('.svg') == layer.name.length - 4){
// We have svg
layer.is_vector = true;
}
if (config.layers.length == 1 && (config.layer.width == 0 || config.layer.width === null)
&& (config.layer.height == 0 || config.layer.height === null) && config.layer.data == null) {
// Remove first empty layer
this.delete_layer_action = new app.Actions.Delete_layer_action(config.layer.id, true);
await this.delete_layer_action.do();
}
if (layer.link == null) {
if (typeof layer.data == 'object') {
// Load actual image
if (layer.width == 0 || layer.width === null)
layer.width = layer.data.width;
if (layer.height == 0 || layer.height === null)
layer.height = layer.data.height;
layer.link = layer.data.cloneNode(true);
layer.link.onload = function () {
config.need_render = true;
};
layer.data = null;
autoresize_as = [layer.width, layer.height, null, true, true];
//need_autoresize = true;
}
else if (typeof layer.data == 'string') {
image_load_promise = new Promise((resolve, reject) => {
// Try loading as imageData
layer.link = new Image();
layer.link.onload = () => {
// Update dimensions
if (layer.width == 0 || layer.width === null)
layer.width = layer.link.width;
if (layer.height == 0 || layer.height === null)
layer.height = layer.link.height;
if (layer.width_original == null)
layer.width_original = layer.width;
if (layer.height_original == null)
layer.height_original = layer.height;
// Free data
layer.data = null;
autoresize_as = [layer.width, layer.height, layer.id, this.can_automate, true];
config.need_render = true;
resolve();
};
layer.link.onerror = (error) => {
resolve(error);
alertify.error('Sorry, image could not be loaded.');
};
layer.link.src = layer.data;
layer.link.crossOrigin = "Anonymous";
});
}
else {
alertify.error('Error: can not load image.');
}
}
}
if (this.settings != undefined && config.layers.length > 0
&& (config.layer.width == 0 || config.layer.width === null) && (config.layer.height == 0 || config.layer.height === null)
&& config.layer.data == null && layer.type != 'image' && this.can_automate !== false) {
// Update existing layer, because it's empty
this.update_layer_action = new app.Actions.Update_layer_action(config.layer.id, layer);
await this.update_layer_action.do();
}
else {
// Create new layer
config.layers.push(layer);
config.layer = app.Layers.get_layer(layer.id);
app.Layers.auto_increment++;
if (config.layer == null) {
config.layer = config.layers[0];
}
this.inserted_layer_id = layer.id;
}
if (layer.id >= app.Layers.auto_increment)
app.Layers.auto_increment = layer.id + 1;
if (image_load_promise) {
await image_load_promise;
}
if (autoresize_as) {
this.autoresize_canvas_action = new app.Actions.Autoresize_canvas_action(...autoresize_as);
try {
await this.autoresize_canvas_action.do();
} catch(error) {
this.autoresize_canvas_action = null;
}
}
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
app.Layers.auto_increment = this.previous_auto_increment;
if (this.autoresize_canvas_action) {
await this.autoresize_canvas_action.undo();
this.autoresize_canvas_action = null;
}
if (this.inserted_layer_id) {
config.layers.pop();
this.inserted_layer_id = null;
}
if (this.update_layer_action) {
await this.update_layer_action.undo();
this.update_layer_action.free();
this.update_layer_action = null;
}
if (this.delete_layer_action) {
await this.delete_layer_action.undo();
this.delete_layer_action.free();
this.delete_layer_action = null;
}
config.layer = this.previous_selected_layer;
this.previous_selected_layer = null;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
free() {
if (this.delete_layer_action) {
this.delete_layer_action.free();
this.delete_layer_action = null;
}
if (this.update_layer_action) {
this.update_layer_action.free();
this.update_layer_action = null;
}
this.previous_selected_layer = null;
}
}
+29
View File
@@ -0,0 +1,29 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Prepare_canvas_action extends Base_action {
/**
* Resizes/renders the canvas at the specified step. Usually used on both sides of a config update action.
*
* @param {boolean} call_when
*/
constructor(call_when = 'undo') {
super('prepare_canvas', 'Prepare Canvas');
this.call_when = call_when;
}
async do() {
super.do();
if (this.call_when === 'do') {
app.GUI.prepare_canvas();
}
}
async undo() {
super.undo();
if (this.call_when === 'undo') {
app.GUI.prepare_canvas();
}
}
}
@@ -0,0 +1,29 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Refresh_action_attributes_action extends Base_action {
/**
* Resizes/renders the canvas at the specified step. Usually used on both sides of a config update action.
*
* @param {boolean} call_when
*/
constructor(call_when = 'undo') {
super('refresh_action_attributes', 'Refresh Action Attributes');
this.call_when = call_when;
}
async do() {
super.do();
if (this.call_when === 'do') {
app.GUI.GUI_tools.show_action_attributes();
}
}
async undo() {
super.undo();
if (this.call_when === 'undo') {
app.GUI.GUI_tools.show_action_attributes();
}
}
}
@@ -0,0 +1,29 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Refresh_layers_gui_action extends Base_action {
/**
* Resizes/renders the canvas at the specified step. Usually used on both sides of a config update action.
*
* @param {boolean} call_when
*/
constructor(call_when = 'undo') {
super('refresh_gui', 'Refresh GUI');
this.call_when = call_when;
}
async do() {
super.do();
if (this.call_when === 'do') {
app.Layers.refresh_gui();
}
}
async undo() {
super.undo();
if (this.call_when === 'undo') {
app.Layers.refresh_gui();
}
}
}
+64
View File
@@ -0,0 +1,64 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Reorder_layer_action extends Base_action {
/**
* Reorder layer up or down in the layer stack
*
* @param {int} layer_id
* @param {int} direction
*/
constructor(layer_id, direction) {
super('reorder_layer', 'Reorder Layer');
this.layer_id = parseInt(layer_id);
this.direction = direction;
this.reference_layer = null;
this.reference_target = null;
this.old_layer_order = null;
this.old_target_order = null;
}
async do() {
super.do();
this.reference_layer = app.Layers.get_layer(this.layer_id);
if (!this.reference_layer) {
throw new Error('Aborted - layer with specified id doesn\'t exist');
}
if (this.direction < 0) {
this.reference_target = app.Layers.find_previous(this.layer_id);
}
else {
this.reference_target = app.Layers.find_next(this.layer_id);
}
if (!this.reference_target) {
throw new Error('Aborted - layer has nowhere to move');
}
this.old_layer_order = this.reference_layer.order;
this.old_target_order = this.reference_target.order;
this.reference_layer.order = this.old_target_order;
this.reference_target.order = this.old_layer_order;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.reference_layer) {
this.reference_layer.order = this.old_layer_order;
this.reference_layer = null;
}
if (this.reference_target) {
this.reference_target.order = this.old_target_order;
this.reference_target = null;
}
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
free() {
this.reference_layer = null;
this.reference_target = null;
}
}
+66
View File
@@ -0,0 +1,66 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Reset_layers_action extends Base_action {
/*
* removes all layers
*/
constructor(auto_insert) {
super('reset_layers', 'Reset Layers');
this.auto_insert = auto_insert;
this.previous_auto_increment = null;
this.delete_actions = null;
this.insert_action = null;
}
async do() {
super.do();
const auto_insert = this.auto_insert;
this.previous_auto_increment = app.Layers.auto_increment;
this.delete_actions = [];
for (let i = config.layers.length - 1; i >= 0; i--) {
const delete_action = new app.Actions.Delete_layer_action(config.layers[i].id, true);
await delete_action.do();
this.delete_actions.push(delete_action);
}
app.Layers.auto_increment = 1;
if (auto_insert != undefined && auto_insert === true) {
const settings = {};
this.insert_action = new app.Actions.Insert_layer_action(settings);
await this.insert_action.do();
}
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.insert_action) {
await this.insert_action.undo();
this.insert_action.free();
this.insert_action = null;
}
for (let i = this.delete_actions.length - 1; i >= 0; i--) {
await this.delete_actions[i].undo();
this.delete_actions[i].free();
}
app.Layers.auto_increment = this.previous_auto_increment;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
free() {
if (this.insert_action) {
this.insert_action.free();
this.insert_action = null;
}
if (this.delete_actions) {
for (let action of this.delete_actions) {
action.free();
}
this.delete_actions = null;
}
}
}
@@ -0,0 +1,57 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Reset_selection_action extends Base_action {
/**
* Sets the selection to empty
*
* @prop {object} [mirror_selection_settings] - Optional object to also set to an empty selection object
*/
constructor(mirror_selection_settings) {
super('reset_selection', 'Reset Selection');
this.mirror_selection_settings = mirror_selection_settings;
this.settings_reference = null;
this.old_settings_data = null;
}
async do() {
super.do();
this.settings_reference = app.Layers.Base_selection.find_settings();
this.old_settings_data = JSON.parse(JSON.stringify(this.settings_reference.data));
this.settings_reference.data = {
x: null,
y: null,
width: null,
height: null
}
if (this.mirror_selection_settings) {
this.mirror_selection_settings.x = null;
this.mirror_selection_settings.y = null;
this.mirror_selection_settings.width = null;
this.mirror_selection_settings.height = null;
}
config.need_render = true;
}
async undo() {
super.undo();
if (this.old_settings_data) {
for (let prop of ['x', 'y', 'width', 'height']) {
this.settings_reference.data[prop] = this.old_settings_data[prop];
if (this.mirror_selection_settings) {
this.mirror_selection_settings[prop] = this.old_settings_data[prop];
}
}
}
this.settings_reference = null;
this.old_settings_data = null;
config.need_render = true;
}
free() {
this.settings_reference = null;
this.old_settings_data = null;
this.mirror_selection_settings = null;
}
}
+57
View File
@@ -0,0 +1,57 @@
import app from '../app.js';
import config from '../config.js';
import { Base_action } from './base.js';
export class Select_layer_action extends Base_action {
/**
* marks layer as selected, active
*
* @param {int} layer_id
*/
constructor(layer_id, ignore_same_selection = false) {
super('select_layer', 'Select Layer');
this.reset_selection_action = null;
this.layer_id = parseInt(layer_id);
this.ignore_same_selection = ignore_same_selection;
this.old_layer = null;
}
async do() {
super.do();
let old_layer = config.layer;
let new_layer = app.Layers.get_layer(this.layer_id);
if (old_layer !== new_layer) {
this.old_layer = old_layer;
config.layer = new_layer;
} else if (!this.ignore_same_selection) {
throw new Error('Aborted - Layer already selected');
}
this.reset_selection_action = new app.Actions.Reset_selection_action();
await this.reset_selection_action.do();
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
if (this.reset_selection_action) {
await this.reset_selection_action.undo();
this.reset_selection_action = null;
}
config.layer = this.old_layer;
this.old_layer = null;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
free() {
this.old_layer = null;
}
}
@@ -0,0 +1,33 @@
import app from './../app.js';
import config from './../config.js';
import { Base_action } from './base.js';
export class Select_next_layer_action extends Base_action {
constructor(reference_layer_id) {
super('select_next_layer', 'Select Next Layer');
this.reference_layer_id = reference_layer_id;
this.old_config_layer = null;
}
async do() {
super.do();
const next_layer = app.Layers.find_next(this.reference_layer_id);
if (!next_layer) {
throw new Error('Aborted - Next layer to select not found');
}
this.old_config_layer = config.layer;
config.layer = next_layer;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
async undo() {
super.undo();
config.layer = this.old_config_layer;
this.old_config_layer = null;
app.Layers.render();
app.GUI.GUI_layers.render_layers();
}
}

Some files were not shown because too many files have changed in this diff Show More