Merge pull request #30 from outis1one/claude/migrate-to-minipaint-eYKWf

Fix selection tools and improve UI/UX
This commit is contained in:
Outis
2026-01-26 22:33:00 -05:00
committed by GitHub
5 changed files with 89 additions and 44 deletions
+4
View File
@@ -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
View File
@@ -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>
+22 -2
View File
@@ -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;
+5 -4
View File
@@ -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;
} }
+56 -36
View File
@@ -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.');
} }
/** /**