Add miniPaint as new frontend base
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
["@babel/plugin-transform-runtime", {
|
||||
"regenerator": true
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||

|
||||
(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.
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 122 B |
@@ -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 |
|
After Width: | Height: | Size: 265 B |
|
After Width: | Height: | Size: 84 B |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 198 KiB |
@@ -1,12 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Photo Edit</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
<html dir="ltr" lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
||||
<title>miniPaint - image editor</title>
|
||||
<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..." />
|
||||
<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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
|
||||
<link rel="icon" sizes="192x192" href="images/favicon.png">
|
||||
<!-- <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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +1,51 @@
|
||||
{
|
||||
"name": "ai-photo-edit-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"name": "miniPaint",
|
||||
"version": "4.14.2",
|
||||
"author": "Vilius L.",
|
||||
"description": "Online graphics editing tool lets create, edit images using HTML5 technologies.",
|
||||
"keywords": [
|
||||
"canvas",
|
||||
"drawing",
|
||||
"paint",
|
||||
"layers",
|
||||
"effects"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint ."
|
||||
"server": "webpack serve --mode development --env development --open",
|
||||
"dev": "webpack --mode development",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"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": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"fabric": "^5.3.0",
|
||||
"axios": "^1.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"vite": "^5.0.12"
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"alertifyjs": "^1.13.1",
|
||||
"blueimp-canvas-to-blob": "^3.28.0",
|
||||
"exif-js": "^2.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuzzysort": "^1.1.4",
|
||||
"gif.js.optimized": "^1.0.1",
|
||||
"hermite-resize": "git+https://github.com/viliusle/Hermite-resize.git",
|
||||
"jquery": "^3.5.1",
|
||||
"pica": "^7.0.0",
|
||||
"semver-compare": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "^1.6.28"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)); }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Managing Undo History with Actions
|
||||
|
||||
More information on wiki page:
|
||||
|
||||
https://github.com/viliusle/miniPaint/wiki/Undo-Redo-system
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||