Fix selection tools and improve UI/UX
- Fix cut/copy to preserve mask shape with transparency - Add missing CSS icons for magic_wand, lasso, ellipse_select tools - Fix toast messages styling (visible on dark theme) - Update AI Inpaint to work with all selection tools - Change tab title to +miniPaint - Add DNS config to docker-compose for external API access
This commit is contained in:
@@ -17,6 +17,10 @@ services:
|
|||||||
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
||||||
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
||||||
- CORS_ORIGINS=*
|
- CORS_ORIGINS=*
|
||||||
|
# DNS servers for reliable external API access (Replicate, etc.)
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
||||||
<title>miniPaint - image editor</title>
|
<title>+miniPaint - AI 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="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="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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<nav aria-label="Main Menu" class="main_menu" id="main_menu"></nav>
|
<nav aria-label="Main Menu" class="main_menu" id="main_menu"></nav>
|
||||||
|
|
||||||
<div class="submenu">
|
<div class="submenu">
|
||||||
<a class="logo" href="#">miniPaint</a>
|
<a class="logo" href="#">+miniPaint</a>
|
||||||
<div class="block attributes" id="action_attributes"></div>
|
<div class="block attributes" id="action_attributes"></div>
|
||||||
<button class="undo_button" id="undo_button" type="button">
|
<button class="undo_button" id="undo_button" type="button">
|
||||||
<span class="sr_only">Undo</span>
|
<span class="sr_only">Undo</span>
|
||||||
|
|||||||
@@ -323,6 +323,9 @@ IMPORTANT: any new icon should also must be added on /service-worker.js + its ve
|
|||||||
.sidebar_left .animation:after{ background-image: url('images/icons/animation.svg'); }
|
.sidebar_left .animation:after{ background-image: url('images/icons/animation.svg'); }
|
||||||
.sidebar_left .smart_select:after{ background-image: url('images/icons/smart_select.svg'); }
|
.sidebar_left .smart_select:after{ background-image: url('images/icons/smart_select.svg'); }
|
||||||
.sidebar_left .ai_inpaint:after{ background-image: url('images/icons/ai_inpaint.svg'); }
|
.sidebar_left .ai_inpaint:after{ background-image: url('images/icons/ai_inpaint.svg'); }
|
||||||
|
.sidebar_left .magic_wand:after{ background-image: url('images/icons/magic_wand.svg'); }
|
||||||
|
.sidebar_left .lasso:after{ background-image: url('images/icons/lasso.svg'); }
|
||||||
|
.sidebar_left .ellipse_select:after{ background-image: url('images/icons/ellipse_select.svg'); }
|
||||||
|
|
||||||
@media screen and (max-width:550px){
|
@media screen and (max-width:550px){
|
||||||
#sidebar_left{
|
#sidebar_left{
|
||||||
@@ -722,8 +725,25 @@ canvas{
|
|||||||
height: 0;
|
height: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.alertify-notifier{
|
/* Alertify toast notification styling */
|
||||||
color: black;
|
.alertify-notifier .ajs-message {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.alertify-notifier .ajs-message.ajs-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.alertify-notifier .ajs-message.ajs-error {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.alertify-notifier .ajs-message.ajs-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.effectsPreview{
|
.effectsPreview{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* AI Inpaint Tool - Edit selected regions using AI with text prompts
|
* AI Inpaint Tool - Edit selected regions using AI with text prompts
|
||||||
* Works with Smart Select tool's mask or manual selection
|
* Works with any selection tool: Smart Select, Magic Wand, Lasso, Ellipse Select, or Selection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import app from './../app.js';
|
import app from './../app.js';
|
||||||
@@ -38,12 +38,13 @@ class Ai_inpaint_class extends Base_tools_class {
|
|||||||
showInpaintDialog() {
|
showInpaintDialog() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
// Check if we have a selection
|
// Check if we have a selection from any selection tool
|
||||||
var hasMask = window.smartSelectMask != null;
|
// All selection tools (Smart Select, Magic Wand, Lasso, Ellipse Select) store their mask in window.smartSelectMask
|
||||||
|
var hasMask = window.smartSelectMask != null && window.smartSelectMask.canvas != null;
|
||||||
var hasRectSelection = this.getRectSelection() != null;
|
var hasRectSelection = this.getRectSelection() != null;
|
||||||
|
|
||||||
if (!hasMask && !hasRectSelection) {
|
if (!hasMask && !hasRectSelection) {
|
||||||
alertify.warning('No selection found. Use Smart Select or Selection tool first.');
|
alertify.warning('No selection found. Use Smart Select, Magic Wand, Lasso, Ellipse Select, or Selection tool first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,8 +394,11 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy selected area to a new layer
|
* Copy selected area to a new layer
|
||||||
|
* The result preserves the mask shape with transparency
|
||||||
*/
|
*/
|
||||||
copyToLayer() {
|
copyToLayer() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
if (!this.currentMask || !this.maskCanvas) {
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
alertify.error('No selection to copy');
|
alertify.error('No selection to copy');
|
||||||
return;
|
return;
|
||||||
@@ -409,27 +412,27 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
// Get the bounds of the selection
|
// Get the bounds of the selection
|
||||||
var bounds = this.selectionBounds;
|
var bounds = this.selectionBounds;
|
||||||
if (!bounds || !bounds.origMinX === undefined) {
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
alertify.error('Invalid selection bounds');
|
alertify.error('Invalid selection bounds');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create canvas with just the selected pixels
|
// Create canvas with just the selected pixels (masked)
|
||||||
var canvas = document.createElement('canvas');
|
var maskedCanvas = document.createElement('canvas');
|
||||||
canvas.width = layer.width_original;
|
maskedCanvas.width = layer.width_original;
|
||||||
canvas.height = layer.height_original;
|
maskedCanvas.height = layer.height_original;
|
||||||
var ctx = canvas.getContext('2d');
|
var maskedCtx = maskedCanvas.getContext('2d');
|
||||||
|
|
||||||
// Draw original image
|
// Draw original image
|
||||||
ctx.drawImage(layer.link, 0, 0);
|
maskedCtx.drawImage(layer.link, 0, 0);
|
||||||
|
|
||||||
// Apply mask - keep only selected pixels
|
// Apply mask - keep only selected pixels (this creates the shape!)
|
||||||
ctx.globalCompositeOperation = 'destination-in';
|
maskedCtx.globalCompositeOperation = 'destination-in';
|
||||||
ctx.drawImage(this.maskCanvas, 0, 0);
|
maskedCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
// Crop to selection bounds
|
// Crop to selection bounds (still preserves transparency within the crop)
|
||||||
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
var cropWidth = bounds.origMaxX - bounds.origMinX + 1;
|
||||||
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
var cropHeight = bounds.origMaxY - bounds.origMinY + 1;
|
||||||
|
|
||||||
if (cropWidth <= 0 || cropHeight <= 0) {
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
alertify.error('Selection is too small');
|
alertify.error('Selection is too small');
|
||||||
@@ -441,8 +444,9 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
croppedCanvas.height = cropHeight;
|
croppedCanvas.height = cropHeight;
|
||||||
var croppedCtx = croppedCanvas.getContext('2d');
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Copy only the selected region (transparency is preserved)
|
||||||
croppedCtx.drawImage(
|
croppedCtx.drawImage(
|
||||||
canvas,
|
maskedCanvas,
|
||||||
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
0, 0, cropWidth, cropHeight
|
0, 0, cropWidth, cropHeight
|
||||||
);
|
);
|
||||||
@@ -455,12 +459,12 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
var params = {
|
var params = {
|
||||||
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
width: cropWidth,
|
width: Math.round(cropWidth * scaleX),
|
||||||
height: cropHeight,
|
height: Math.round(cropHeight * scaleY),
|
||||||
width_original: cropWidth,
|
width_original: cropWidth,
|
||||||
height_original: cropHeight,
|
height_original: cropHeight,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
name: 'AI Selection Copy',
|
name: config.layer.name + ' (Selection)',
|
||||||
data: croppedCanvas.toDataURL('image/png')
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -470,13 +474,22 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
alertify.success('Selection copied to new layer!');
|
// Enable transparency so the user can see the mask shape
|
||||||
|
if (config.TRANSPARENCY == false) {
|
||||||
|
config.TRANSPARENCY = true;
|
||||||
|
_this.Base_layers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
alertify.success('Selection copied to new layer! Switch to Select tool to move it.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cut selected area to a new layer (copy + delete from original)
|
* Cut selected area to a new layer (copy + delete from original)
|
||||||
|
* The result preserves the mask shape with transparency
|
||||||
*/
|
*/
|
||||||
cutToLayer() {
|
cutToLayer() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
if (!this.currentMask || !this.maskCanvas) {
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
alertify.error('No selection to cut');
|
alertify.error('No selection to cut');
|
||||||
return;
|
return;
|
||||||
@@ -495,22 +508,22 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create canvas with just the selected pixels
|
// Create canvas with just the selected pixels (masked)
|
||||||
var canvas = document.createElement('canvas');
|
var maskedCanvas = document.createElement('canvas');
|
||||||
canvas.width = layer.width_original;
|
maskedCanvas.width = layer.width_original;
|
||||||
canvas.height = layer.height_original;
|
maskedCanvas.height = layer.height_original;
|
||||||
var ctx = canvas.getContext('2d');
|
var maskedCtx = maskedCanvas.getContext('2d');
|
||||||
|
|
||||||
// Draw original image
|
// Draw original image
|
||||||
ctx.drawImage(layer.link, 0, 0);
|
maskedCtx.drawImage(layer.link, 0, 0);
|
||||||
|
|
||||||
// Apply mask - keep only selected pixels
|
// Apply mask - keep only selected pixels (this creates the shape!)
|
||||||
ctx.globalCompositeOperation = 'destination-in';
|
maskedCtx.globalCompositeOperation = 'destination-in';
|
||||||
ctx.drawImage(this.maskCanvas, 0, 0);
|
maskedCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
// Crop to selection bounds
|
// Crop to selection bounds (still preserves transparency within the crop)
|
||||||
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
var cropWidth = bounds.origMaxX - bounds.origMinX + 1;
|
||||||
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
var cropHeight = bounds.origMaxY - bounds.origMinY + 1;
|
||||||
|
|
||||||
if (cropWidth <= 0 || cropHeight <= 0) {
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
alertify.error('Selection is too small');
|
alertify.error('Selection is too small');
|
||||||
@@ -522,8 +535,9 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
croppedCanvas.height = cropHeight;
|
croppedCanvas.height = cropHeight;
|
||||||
var croppedCtx = croppedCanvas.getContext('2d');
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Copy only the selected region (transparency is preserved)
|
||||||
croppedCtx.drawImage(
|
croppedCtx.drawImage(
|
||||||
canvas,
|
maskedCanvas,
|
||||||
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
0, 0, cropWidth, cropHeight
|
0, 0, cropWidth, cropHeight
|
||||||
);
|
);
|
||||||
@@ -536,12 +550,12 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
var params = {
|
var params = {
|
||||||
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
width: cropWidth,
|
width: Math.round(cropWidth * scaleX),
|
||||||
height: cropHeight,
|
height: Math.round(cropHeight * scaleY),
|
||||||
width_original: cropWidth,
|
width_original: cropWidth,
|
||||||
height_original: cropHeight,
|
height_original: cropHeight,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
name: 'AI Selection Cut',
|
name: config.layer.name + ' (Cut)',
|
||||||
data: croppedCanvas.toDataURL('image/png')
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -554,7 +568,7 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
// Draw original image
|
// Draw original image
|
||||||
holeCtx.drawImage(layer.link, 0, 0);
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
|
||||||
// Cut out the mask area
|
// Cut out the mask area (creates transparent hole in original shape)
|
||||||
holeCtx.globalCompositeOperation = 'destination-out';
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
@@ -566,10 +580,16 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enable transparency so the user can see the mask shape
|
||||||
|
if (config.TRANSPARENCY == false) {
|
||||||
|
config.TRANSPARENCY = true;
|
||||||
|
_this.Base_layers.render();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the selection
|
// Clear the selection
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
|
|
||||||
alertify.success('Selection cut to new layer!');
|
alertify.success('Selection cut to new layer! Switch to Select tool to move it.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user