Merge pull request #33 from outis1one/claude/migrate-to-minipaint-eYKWf
Claude/migrate to minipaint e yk wf
This commit is contained in:
@@ -14,8 +14,9 @@ pydantic-settings==2.1.0
|
||||
email-validator==2.1.0
|
||||
opencv-python-headless==4.9.0.80
|
||||
scikit-image==0.22.0
|
||||
# rembg for background removal - use onnxruntime 1.15.1 to avoid executable stack issue
|
||||
onnxruntime==1.15.1
|
||||
rembg==2.0.50
|
||||
onnxruntime==1.16.3
|
||||
# SAM (Segment Anything) for smart object selection - runs locally, no API needed
|
||||
torch==2.1.2
|
||||
torchvision==0.16.2
|
||||
|
||||
@@ -518,6 +518,127 @@ IMPORTANT: any new icon should also must be added on /service-worker.js + its ve
|
||||
bottom:0;
|
||||
width: 20px;
|
||||
}
|
||||
/* Layer context menu */
|
||||
.layer_context_menu{
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: var(--section-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
min-width: 150px;
|
||||
}
|
||||
.layer_context_menu ul{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.layer_context_menu li{
|
||||
padding: 6px 15px;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.layer_context_menu li:hover{
|
||||
background: var(--background-color-active);
|
||||
color: var(--text-color-active);
|
||||
}
|
||||
.layer_context_menu li.separator{
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 5px 10px;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.layer_context_menu li.separator:hover{
|
||||
background: var(--border-color);
|
||||
}
|
||||
.layer_scale{
|
||||
display:inline-block;
|
||||
padding:1px 8px;
|
||||
margin-right: 5px;
|
||||
border:1px solid #444;
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-color);
|
||||
cursor:pointer;
|
||||
}
|
||||
/* My Library browser */
|
||||
.library-browser{
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.library-category{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.library-category h3{
|
||||
margin: 0 0 10px 0;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.library-items{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.library-item{
|
||||
width: 120px;
|
||||
padding: 8px;
|
||||
background: var(--area-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.library-item:hover{
|
||||
border-color: var(--background-color-active);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.library-item img{
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
background: repeating-conic-gradient(#808080 0% 25%, #666 0% 50%) 50% / 10px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.library-item-name{
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.library-item-actions{
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.library-item-actions button{
|
||||
flex: 1;
|
||||
padding: 3px 5px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
background: var(--section-background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
.library-item-actions .insert-btn{
|
||||
background: var(--background-color-active);
|
||||
color: var(--text-color-active);
|
||||
}
|
||||
.library-item-actions .delete-btn:hover{
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
.library-dialog .dialog_content{
|
||||
min-width: 500px;
|
||||
}
|
||||
.sidebar_right .label{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,44 @@ const menuDefinition = [
|
||||
name: 'Quick Load',
|
||||
shortcut: 'F10',
|
||||
target: 'file/quickload.quickload'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'My Library',
|
||||
children: [
|
||||
{
|
||||
name: 'Browse Library',
|
||||
ellipsis: true,
|
||||
target: 'file/my_library.browse_library'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'Save Layer to Library',
|
||||
ellipsis: true,
|
||||
target: 'file/my_library.save_to_library'
|
||||
},
|
||||
{
|
||||
name: 'Save Selection to Library',
|
||||
ellipsis: true,
|
||||
target: 'file/my_library.save_selection_to_library'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'Export Library (Backup)',
|
||||
target: 'file/my_library.export_library'
|
||||
},
|
||||
{
|
||||
name: 'Import Library',
|
||||
ellipsis: true,
|
||||
target: 'file/my_library.import_library'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -291,6 +329,29 @@ const menuDefinition = [
|
||||
name: 'Remove Background (AI)',
|
||||
ellipsis: true,
|
||||
target: 'image/remove_background.remove_background'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'Selection Effects',
|
||||
children: [
|
||||
{
|
||||
name: 'Invert Selection',
|
||||
ellipsis: true,
|
||||
target: 'image/selection_effects.invert_selection'
|
||||
},
|
||||
{
|
||||
name: 'Adjust Selection',
|
||||
ellipsis: true,
|
||||
target: 'image/selection_effects.adjust_selection'
|
||||
},
|
||||
{
|
||||
name: 'Greyscale Selection',
|
||||
ellipsis: true,
|
||||
target: 'image/selection_effects.greyscale_selection'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -326,6 +387,11 @@ const menuDefinition = [
|
||||
name: 'Convert to Raster',
|
||||
target: 'layer/raster.raster'
|
||||
},
|
||||
{
|
||||
name: 'Scale Layer',
|
||||
ellipsis: true,
|
||||
target: 'layer/scale.scale'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
@@ -480,6 +546,11 @@ const menuDefinition = [
|
||||
ellipsis: true,
|
||||
target: 'effects/black_and_white.black_and_white'
|
||||
},
|
||||
{
|
||||
name: 'Greyscale',
|
||||
ellipsis: true,
|
||||
target: 'effects/greyscale.greyscale'
|
||||
},
|
||||
{
|
||||
name: 'Borders',
|
||||
ellipsis: true,
|
||||
|
||||
@@ -11,17 +11,22 @@ import Layer_rename_class from './../../modules/layer/rename.js';
|
||||
import Effects_browser_class from './../../modules/effects/browser.js';
|
||||
import Layer_duplicate_class from './../../modules/layer/duplicate.js';
|
||||
import Layer_raster_class from './../../modules/layer/raster.js';
|
||||
import Layer_scale_class from './../../modules/layer/scale.js';
|
||||
import Layer_merge_class from './../../modules/layer/merge.js';
|
||||
import Layer_flatten_class from './../../modules/layer/flatten.js';
|
||||
import Tools_translate_class from './../../modules/tools/translate.js';
|
||||
|
||||
var template = `
|
||||
<button type="button" class="layer_add trn" id="insert_layer" title="Insert new layer">+</button>
|
||||
<button type="button" class="layer_duplicate trn" id="layer_duplicate" title="Duplicate layer">D</button>
|
||||
<button type="button" class="layer_raster trn" id="layer_raster" title="Convert layer to raster">R</button>
|
||||
<button type="button" class="layer_scale trn" id="layer_scale" title="Scale layer">S</button>
|
||||
|
||||
<button type="button" class="layers_arrow trn" title="Move layer down" id="layer_down">↓</button>
|
||||
<button type="button" class="layers_arrow trn" title="Move layer up" id="layer_up">↑</button>
|
||||
|
||||
<div class="layers_list" id="layers"></div>
|
||||
<div class="layer_context_menu" id="layer_context_menu"></div>
|
||||
`;
|
||||
|
||||
/**
|
||||
@@ -36,7 +41,11 @@ class GUI_layers_class {
|
||||
this.Effects_browser = new Effects_browser_class();
|
||||
this.Layer_duplicate = new Layer_duplicate_class();
|
||||
this.Layer_raster = new Layer_raster_class();
|
||||
this.Layer_scale = new Layer_scale_class();
|
||||
this.Layer_merge = new Layer_merge_class();
|
||||
this.Layer_flatten = new Layer_flatten_class();
|
||||
this.Tools_translate = new Tools_translate_class();
|
||||
this.contextMenuLayerId = null;
|
||||
}
|
||||
|
||||
render_main_layers() {
|
||||
@@ -67,6 +76,10 @@ class GUI_layers_class {
|
||||
//raster
|
||||
_this.Layer_raster.raster();
|
||||
}
|
||||
else if (target.id == 'layer_scale') {
|
||||
//scale
|
||||
_this.Layer_scale.scale();
|
||||
}
|
||||
else if (target.id == 'layer_up') {
|
||||
//move layer up
|
||||
app.State.do_action(
|
||||
@@ -127,6 +140,134 @@ class GUI_layers_class {
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click context menu for layers
|
||||
document.getElementById('layers_base').addEventListener('contextmenu', function (event) {
|
||||
var target = event.target;
|
||||
|
||||
// Check if right-clicked on a layer item
|
||||
if (target.id == 'layer_name' || target.closest('.item')) {
|
||||
event.preventDefault();
|
||||
|
||||
var layerId = target.dataset.id || target.closest('.item').querySelector('[data-id]').dataset.id;
|
||||
_this.showContextMenu(event.clientX, event.clientY, layerId);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide context menu when clicking elsewhere
|
||||
document.addEventListener('click', function (event) {
|
||||
_this.hideContextMenu();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Show context menu for layer
|
||||
*/
|
||||
showContextMenu(x, y, layerId) {
|
||||
var _this = this;
|
||||
this.contextMenuLayerId = layerId;
|
||||
|
||||
// Select the layer first
|
||||
if (layerId != config.layer.id) {
|
||||
app.State.do_action(
|
||||
new app.Actions.Select_layer_action(layerId)
|
||||
);
|
||||
}
|
||||
|
||||
var menuItems = [
|
||||
{ label: 'Rename', action: 'rename' },
|
||||
{ label: 'Duplicate', action: 'duplicate' },
|
||||
{ label: 'Delete', action: 'delete' },
|
||||
{ label: '---' },
|
||||
{ label: 'Move Up', action: 'move_up' },
|
||||
{ label: 'Move Down', action: 'move_down' },
|
||||
{ label: '---' },
|
||||
{ label: 'Scale Layer...', action: 'scale' },
|
||||
{ label: 'Convert to Raster', action: 'raster' },
|
||||
{ label: '---' },
|
||||
{ label: 'Merge Down', action: 'merge' },
|
||||
{ label: 'Flatten All', action: 'flatten' },
|
||||
];
|
||||
|
||||
var menu = document.getElementById('layer_context_menu');
|
||||
var html = '<ul class="context-menu-list">';
|
||||
|
||||
for (var i = 0; i < menuItems.length; i++) {
|
||||
var item = menuItems[i];
|
||||
if (item.label === '---') {
|
||||
html += '<li class="separator"></li>';
|
||||
} else {
|
||||
html += '<li data-action="' + item.action + '">' + item.label + '</li>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</ul>';
|
||||
menu.innerHTML = html;
|
||||
menu.style.display = 'block';
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
|
||||
// Add click handlers to menu items
|
||||
menu.querySelectorAll('li[data-action]').forEach(function(item) {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
_this.handleContextMenuAction(this.dataset.action);
|
||||
_this.hideContextMenu();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide context menu
|
||||
*/
|
||||
hideContextMenu() {
|
||||
var menu = document.getElementById('layer_context_menu');
|
||||
if (menu) {
|
||||
menu.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle context menu action
|
||||
*/
|
||||
handleContextMenuAction(action) {
|
||||
var layerId = this.contextMenuLayerId;
|
||||
|
||||
switch (action) {
|
||||
case 'rename':
|
||||
this.Layer_rename.rename(layerId);
|
||||
break;
|
||||
case 'duplicate':
|
||||
this.Layer_duplicate.duplicate();
|
||||
break;
|
||||
case 'delete':
|
||||
app.State.do_action(
|
||||
new app.Actions.Delete_layer_action(layerId)
|
||||
);
|
||||
break;
|
||||
case 'move_up':
|
||||
app.State.do_action(
|
||||
new app.Actions.Reorder_layer_action(layerId, 1)
|
||||
);
|
||||
break;
|
||||
case 'move_down':
|
||||
app.State.do_action(
|
||||
new app.Actions.Reorder_layer_action(layerId, -1)
|
||||
);
|
||||
break;
|
||||
case 'scale':
|
||||
this.Layer_scale.scale();
|
||||
break;
|
||||
case 'raster':
|
||||
this.Layer_raster.raster();
|
||||
break;
|
||||
case 'merge':
|
||||
this.Layer_merge.merge();
|
||||
break;
|
||||
case 'flatten':
|
||||
this.Layer_flatten.flatten();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Greyscale Effect - Convert layer to greyscale (desaturate)
|
||||
* Useful for CNC carving, depth maps, etc.
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import Base_layers_class from './../../core/base-layers.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Effects_greyscale_class {
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
instance = this;
|
||||
|
||||
this.POP = new Dialog_class();
|
||||
this.Base_layers = new Base_layers_class();
|
||||
}
|
||||
|
||||
greyscale() {
|
||||
var _this = this;
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('This layer must contain an image. Please convert it to raster first.');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Convert to Greyscale',
|
||||
preview: true,
|
||||
effects: true,
|
||||
params: [
|
||||
{
|
||||
name: "method",
|
||||
title: "Method:",
|
||||
values: ["Luminosity (Rec. 709)", "Average", "Lightness", "Red Channel", "Green Channel", "Blue Channel"],
|
||||
value: "Luminosity (Rec. 709)"
|
||||
},
|
||||
{name: "contrast", title: "Contrast:", value: 0, range: [-100, 100]},
|
||||
{name: "brightness", title: "Brightness:", value: 0, range: [-100, 100]},
|
||||
{name: "invert", title: "Invert:", value: false},
|
||||
],
|
||||
on_change: function (params, canvas_preview, w, h) {
|
||||
var img = canvas_preview.getImageData(0, 0, w, h);
|
||||
var data = _this.apply_greyscale(img, params);
|
||||
canvas_preview.putImageData(data, 0, 0);
|
||||
},
|
||||
on_finish: function (params) {
|
||||
_this.save(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
save(params) {
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var data = this.apply_greyscale(img, params);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
|
||||
return app.State.do_action(
|
||||
new app.Actions.Update_layer_image_action(canvas)
|
||||
);
|
||||
}
|
||||
|
||||
apply_greyscale(imageData, params) {
|
||||
var data = imageData.data;
|
||||
var method = params.method;
|
||||
var contrast = (params.contrast || 0) / 100;
|
||||
var brightness = (params.brightness || 0) * 2.55; // Convert to 0-255 range
|
||||
var invert = params.invert || false;
|
||||
|
||||
// Contrast factor
|
||||
var factor = (1 + contrast);
|
||||
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] === 0) continue; // Skip transparent pixels
|
||||
|
||||
var r = data[i];
|
||||
var g = data[i + 1];
|
||||
var b = data[i + 2];
|
||||
var grey;
|
||||
|
||||
// Calculate greyscale value based on method
|
||||
switch (method) {
|
||||
case "Luminosity (Rec. 709)":
|
||||
// Standard HDTV (Rec. 709) - most accurate perceptual
|
||||
grey = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
break;
|
||||
case "Average":
|
||||
grey = (r + g + b) / 3;
|
||||
break;
|
||||
case "Lightness":
|
||||
// HSL lightness
|
||||
grey = (Math.max(r, g, b) + Math.min(r, g, b)) / 2;
|
||||
break;
|
||||
case "Red Channel":
|
||||
grey = r;
|
||||
break;
|
||||
case "Green Channel":
|
||||
grey = g;
|
||||
break;
|
||||
case "Blue Channel":
|
||||
grey = b;
|
||||
break;
|
||||
default:
|
||||
grey = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
// Apply brightness
|
||||
grey += brightness;
|
||||
|
||||
// Apply contrast (around middle grey)
|
||||
grey = ((grey - 128) * factor) + 128;
|
||||
|
||||
// Invert if requested
|
||||
if (invert) {
|
||||
grey = 255 - grey;
|
||||
}
|
||||
|
||||
// Clamp to valid range
|
||||
grey = Math.max(0, Math.min(255, Math.round(grey)));
|
||||
|
||||
data[i] = grey;
|
||||
data[i + 1] = grey;
|
||||
data[i + 2] = grey;
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
demo(canvas_id, canvas_thumb) {
|
||||
var canvas = document.getElementById(canvas_id);
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(canvas_thumb, 0, 0);
|
||||
|
||||
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
|
||||
var params = {
|
||||
method: "Luminosity (Rec. 709)",
|
||||
contrast: 0,
|
||||
brightness: 0,
|
||||
invert: false
|
||||
};
|
||||
var data = this.apply_greyscale(img, params);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export default Effects_greyscale_class;
|
||||
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* My Library - Save and reuse your own assets (clip art, templates, etc.)
|
||||
* Assets are stored in browser's IndexedDB for persistence
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import Base_layers_class from './../../core/base-layers.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
// IndexedDB setup
|
||||
const DB_NAME = 'miniPaintLibrary';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'assets';
|
||||
|
||||
class File_my_library_class {
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
instance = this;
|
||||
|
||||
this.POP = new Dialog_class();
|
||||
this.Base_layers = new Base_layers_class();
|
||||
this.db = null;
|
||||
|
||||
this.initDB();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB
|
||||
*/
|
||||
initDB() {
|
||||
var _this = this;
|
||||
|
||||
var request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = function(event) {
|
||||
console.error('IndexedDB error:', event);
|
||||
};
|
||||
|
||||
request.onsuccess = function(event) {
|
||||
_this.db = event.target.result;
|
||||
};
|
||||
|
||||
request.onupgradeneeded = function(event) {
|
||||
var db = event.target.result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
var store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
||||
store.createIndex('name', 'name', { unique: false });
|
||||
store.createIndex('category', 'category', { unique: false });
|
||||
store.createIndex('created', 'created', { unique: false });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current layer as a library asset
|
||||
*/
|
||||
save_to_library() {
|
||||
var _this = this;
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please select an image layer to save');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Save to My Library',
|
||||
params: [
|
||||
{
|
||||
name: "name",
|
||||
title: "Asset Name:",
|
||||
value: config.layer.name || "My Asset"
|
||||
},
|
||||
{
|
||||
name: "category",
|
||||
title: "Category:",
|
||||
value: "General",
|
||||
values: ["General", "Shapes", "Borders", "Icons", "Templates", "Text Elements", "Backgrounds", "Other"]
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
title: "Description:",
|
||||
value: ""
|
||||
}
|
||||
],
|
||||
on_finish: function (params) {
|
||||
_this.do_save_to_library(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current selection to library (uses mask if available)
|
||||
*/
|
||||
save_selection_to_library() {
|
||||
var _this = this;
|
||||
|
||||
if (!window.smartSelectMask || !window.smartSelectMask.canvas) {
|
||||
alertify.error('No selection. Use a selection tool first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please select an image layer');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Save Selection to Library',
|
||||
params: [
|
||||
{
|
||||
name: "name",
|
||||
title: "Asset Name:",
|
||||
value: "Selection Asset"
|
||||
},
|
||||
{
|
||||
name: "category",
|
||||
title: "Category:",
|
||||
value: "General",
|
||||
values: ["General", "Shapes", "Borders", "Icons", "Templates", "Text Elements", "Backgrounds", "Other"]
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
title: "Description:",
|
||||
value: ""
|
||||
}
|
||||
],
|
||||
on_finish: function (params) {
|
||||
_this.do_save_selection_to_library(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
do_save_to_library(params) {
|
||||
var _this = this;
|
||||
|
||||
// Get canvas from current layer
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id, true, false);
|
||||
|
||||
// Create thumbnail (max 150px)
|
||||
var thumbCanvas = document.createElement('canvas');
|
||||
var maxSize = 150;
|
||||
var scale = Math.min(maxSize / canvas.width, maxSize / canvas.height);
|
||||
thumbCanvas.width = Math.round(canvas.width * scale);
|
||||
thumbCanvas.height = Math.round(canvas.height * scale);
|
||||
var thumbCtx = thumbCanvas.getContext('2d');
|
||||
thumbCtx.drawImage(canvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
||||
|
||||
var asset = {
|
||||
name: params.name,
|
||||
category: params.category,
|
||||
description: params.description,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
data: canvas.toDataURL('image/png'),
|
||||
thumbnail: thumbCanvas.toDataURL('image/png'),
|
||||
created: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveAsset(asset, function() {
|
||||
alertify.success('Saved "' + params.name + '" to library!');
|
||||
});
|
||||
}
|
||||
|
||||
do_save_selection_to_library(params) {
|
||||
var _this = this;
|
||||
var layer = config.layer;
|
||||
var maskCanvas = window.smartSelectMask.canvas;
|
||||
|
||||
// Create canvas with just the selected pixels
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = layer.width_original;
|
||||
canvas.height = layer.height_original;
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.drawImage(layer.link, 0, 0);
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
ctx.drawImage(maskCanvas, 0, 0);
|
||||
|
||||
// Find bounds of selection
|
||||
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var data = imageData.data;
|
||||
var minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0;
|
||||
|
||||
for (var y = 0; y < canvas.height; y++) {
|
||||
for (var x = 0; x < canvas.width; x++) {
|
||||
var i = (y * canvas.width + x) * 4;
|
||||
if (data[i + 3] > 0) {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crop to selection bounds
|
||||
var cropWidth = maxX - minX + 1;
|
||||
var cropHeight = maxY - minY + 1;
|
||||
var croppedCanvas = document.createElement('canvas');
|
||||
croppedCanvas.width = cropWidth;
|
||||
croppedCanvas.height = cropHeight;
|
||||
var croppedCtx = croppedCanvas.getContext('2d');
|
||||
croppedCtx.drawImage(canvas, minX, minY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
||||
|
||||
// Create thumbnail
|
||||
var thumbCanvas = document.createElement('canvas');
|
||||
var maxSize = 150;
|
||||
var scale = Math.min(maxSize / cropWidth, maxSize / cropHeight);
|
||||
thumbCanvas.width = Math.round(cropWidth * scale);
|
||||
thumbCanvas.height = Math.round(cropHeight * scale);
|
||||
var thumbCtx = thumbCanvas.getContext('2d');
|
||||
thumbCtx.drawImage(croppedCanvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
||||
|
||||
var asset = {
|
||||
name: params.name,
|
||||
category: params.category,
|
||||
description: params.description,
|
||||
width: cropWidth,
|
||||
height: cropHeight,
|
||||
data: croppedCanvas.toDataURL('image/png'),
|
||||
thumbnail: thumbCanvas.toDataURL('image/png'),
|
||||
created: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveAsset(asset, function() {
|
||||
alertify.success('Saved selection "' + params.name + '" to library!');
|
||||
});
|
||||
}
|
||||
|
||||
saveAsset(asset, callback) {
|
||||
if (!this.db) {
|
||||
alertify.error('Database not ready, please try again');
|
||||
return;
|
||||
}
|
||||
|
||||
var transaction = this.db.transaction([STORE_NAME], 'readwrite');
|
||||
var store = transaction.objectStore(STORE_NAME);
|
||||
var request = store.add(asset);
|
||||
|
||||
request.onsuccess = function() {
|
||||
if (callback) callback();
|
||||
};
|
||||
|
||||
request.onerror = function(event) {
|
||||
alertify.error('Error saving asset');
|
||||
console.error('Save error:', event);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse and insert assets from library
|
||||
*/
|
||||
browse_library() {
|
||||
var _this = this;
|
||||
|
||||
this.getAllAssets(function(assets) {
|
||||
_this.showLibraryBrowser(assets);
|
||||
});
|
||||
}
|
||||
|
||||
getAllAssets(callback) {
|
||||
if (!this.db) {
|
||||
alertify.error('Database not ready');
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
|
||||
var transaction = this.db.transaction([STORE_NAME], 'readonly');
|
||||
var store = transaction.objectStore(STORE_NAME);
|
||||
var request = store.getAll();
|
||||
|
||||
request.onsuccess = function(event) {
|
||||
callback(event.target.result || []);
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
callback([]);
|
||||
};
|
||||
}
|
||||
|
||||
showLibraryBrowser(assets) {
|
||||
var _this = this;
|
||||
|
||||
if (assets.length === 0) {
|
||||
alertify.warning('Your library is empty. Save some assets first!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
var categories = {};
|
||||
assets.forEach(function(asset) {
|
||||
var cat = asset.category || 'General';
|
||||
if (!categories[cat]) categories[cat] = [];
|
||||
categories[cat].push(asset);
|
||||
});
|
||||
|
||||
// Build HTML for the browser
|
||||
var html = '<div class="library-browser">';
|
||||
html += '<div class="library-categories">';
|
||||
|
||||
for (var cat in categories) {
|
||||
html += '<div class="library-category">';
|
||||
html += '<h3>' + cat + '</h3>';
|
||||
html += '<div class="library-items">';
|
||||
|
||||
categories[cat].forEach(function(asset) {
|
||||
html += '<div class="library-item" data-id="' + asset.id + '">';
|
||||
html += '<img src="' + asset.thumbnail + '" alt="' + asset.name + '" title="' + asset.name + '">';
|
||||
html += '<div class="library-item-name">' + asset.name + '</div>';
|
||||
html += '<div class="library-item-actions">';
|
||||
html += '<button class="insert-btn" data-id="' + asset.id + '">Insert</button>';
|
||||
html += '<button class="delete-btn" data-id="' + asset.id + '">Delete</button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
|
||||
// Show in dialog
|
||||
var settings = {
|
||||
title: 'My Library (' + assets.length + ' assets)',
|
||||
params: [],
|
||||
html: html,
|
||||
className: 'library-dialog',
|
||||
on_load: function(el) {
|
||||
// Add click handlers
|
||||
el.querySelectorAll('.insert-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var id = parseInt(this.dataset.id);
|
||||
_this.insertAsset(id);
|
||||
_this.POP.hide();
|
||||
});
|
||||
});
|
||||
|
||||
el.querySelectorAll('.delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var id = parseInt(this.dataset.id);
|
||||
if (confirm('Delete this asset?')) {
|
||||
_this.deleteAsset(id, function() {
|
||||
alertify.success('Asset deleted');
|
||||
_this.POP.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Double-click to insert
|
||||
el.querySelectorAll('.library-item').forEach(function(item) {
|
||||
item.addEventListener('dblclick', function() {
|
||||
var id = parseInt(this.dataset.id);
|
||||
_this.insertAsset(id);
|
||||
_this.POP.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
insertAsset(id) {
|
||||
var _this = this;
|
||||
|
||||
var transaction = this.db.transaction([STORE_NAME], 'readonly');
|
||||
var store = transaction.objectStore(STORE_NAME);
|
||||
var request = store.get(id);
|
||||
|
||||
request.onsuccess = function(event) {
|
||||
var asset = event.target.result;
|
||||
if (asset) {
|
||||
_this.createLayerFromAsset(asset);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
createLayerFromAsset(asset) {
|
||||
var _this = this;
|
||||
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
// Insert as new layer
|
||||
var params = {
|
||||
x: Math.round((config.WIDTH - asset.width) / 2),
|
||||
y: Math.round((config.HEIGHT - asset.height) / 2),
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
width_original: asset.width,
|
||||
height_original: asset.height,
|
||||
type: 'image',
|
||||
name: asset.name,
|
||||
data: asset.data
|
||||
};
|
||||
|
||||
app.State.do_action(
|
||||
new app.Actions.Bundle_action('insert_library_asset', 'Insert Library Asset', [
|
||||
new app.Actions.Insert_layer_action(params)
|
||||
])
|
||||
);
|
||||
|
||||
alertify.success('Inserted "' + asset.name + '"');
|
||||
};
|
||||
img.src = asset.data;
|
||||
}
|
||||
|
||||
deleteAsset(id, callback) {
|
||||
var transaction = this.db.transaction([STORE_NAME], 'readwrite');
|
||||
var store = transaction.objectStore(STORE_NAME);
|
||||
var request = store.delete(id);
|
||||
|
||||
request.onsuccess = function() {
|
||||
if (callback) callback();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export library to JSON file (backup)
|
||||
*/
|
||||
export_library() {
|
||||
var _this = this;
|
||||
|
||||
this.getAllAssets(function(assets) {
|
||||
if (assets.length === 0) {
|
||||
alertify.warning('Library is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = JSON.stringify(assets, null, 2);
|
||||
var blob = new Blob([data], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'my-library-backup.json';
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
alertify.success('Library exported!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Import library from JSON file
|
||||
*/
|
||||
import_library() {
|
||||
var _this = this;
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = function(e) {
|
||||
var file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
var assets = JSON.parse(event.target.result);
|
||||
|
||||
if (!Array.isArray(assets)) {
|
||||
alertify.error('Invalid library file');
|
||||
return;
|
||||
}
|
||||
|
||||
var imported = 0;
|
||||
assets.forEach(function(asset) {
|
||||
// Remove id so it gets auto-assigned
|
||||
delete asset.id;
|
||||
_this.saveAsset(asset, function() {
|
||||
imported++;
|
||||
if (imported === assets.length) {
|
||||
alertify.success('Imported ' + imported + ' assets!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
alertify.error('Error parsing library file');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
export default File_my_library_class;
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* Selection Effects - Apply effects only to the selected area
|
||||
* Works with any selection tool (Smart Select, Brush Select, Magic Wand, Lasso, Ellipse)
|
||||
* Useful for CNC depth maps where you want to modify specific objects
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import Base_layers_class from './../../core/base-layers.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Image_selection_effects_class {
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
instance = this;
|
||||
|
||||
this.POP = new Dialog_class();
|
||||
this.Base_layers = new Base_layers_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a valid selection
|
||||
*/
|
||||
hasSelection() {
|
||||
return window.smartSelectMask && window.smartSelectMask.canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invert colors only in the selected area
|
||||
*/
|
||||
invert_selection() {
|
||||
var _this = this;
|
||||
|
||||
if (!this.hasSelection()) {
|
||||
alertify.error('No selection. Use a selection tool first (Smart Select, Brush Select, Magic Wand, etc.)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please select an image layer');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Invert Selection',
|
||||
preview: true,
|
||||
params: [
|
||||
{
|
||||
name: "strength",
|
||||
title: "Strength:",
|
||||
value: 100,
|
||||
range: [0, 100]
|
||||
},
|
||||
{
|
||||
name: "preserve_luminosity",
|
||||
title: "Preserve Luminosity:",
|
||||
value: false
|
||||
}
|
||||
],
|
||||
on_change: function (params, canvas_preview, w, h) {
|
||||
var img = canvas_preview.getImageData(0, 0, w, h);
|
||||
var data = _this.apply_invert(img, params);
|
||||
canvas_preview.putImageData(data, 0, 0);
|
||||
},
|
||||
on_finish: function (params) {
|
||||
_this.save_invert(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
apply_invert(imageData, params) {
|
||||
if (!this.hasSelection()) return imageData;
|
||||
|
||||
var data = imageData.data;
|
||||
var width = imageData.width;
|
||||
var height = imageData.height;
|
||||
var strength = (params.strength || 100) / 100;
|
||||
var preserveLuminosity = params.preserve_luminosity || false;
|
||||
|
||||
// Get mask data
|
||||
var maskCanvas = window.smartSelectMask.canvas;
|
||||
var maskCtx = maskCanvas.getContext('2d');
|
||||
|
||||
// Scale mask to match current canvas size if needed
|
||||
var scaledMask = document.createElement('canvas');
|
||||
scaledMask.width = width;
|
||||
scaledMask.height = height;
|
||||
var scaledCtx = scaledMask.getContext('2d');
|
||||
scaledCtx.drawImage(maskCanvas, 0, 0, width, height);
|
||||
|
||||
var maskData = scaledCtx.getImageData(0, 0, width, height).data;
|
||||
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
var maskValue = maskData[i] / 255; // 0-1 range
|
||||
|
||||
if (maskValue > 0.5) { // Inside selection
|
||||
var r = data[i];
|
||||
var g = data[i + 1];
|
||||
var b = data[i + 2];
|
||||
|
||||
// Invert colors
|
||||
var newR = 255 - r;
|
||||
var newG = 255 - g;
|
||||
var newB = 255 - b;
|
||||
|
||||
if (preserveLuminosity) {
|
||||
// Calculate original and new luminosity
|
||||
var oldLum = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
var newLum = 0.299 * newR + 0.587 * newG + 0.114 * newB;
|
||||
|
||||
// Adjust to preserve luminosity
|
||||
if (newLum > 0) {
|
||||
var ratio = oldLum / newLum;
|
||||
newR = Math.min(255, newR * ratio);
|
||||
newG = Math.min(255, newG * ratio);
|
||||
newB = Math.min(255, newB * ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply strength (blend between original and inverted)
|
||||
data[i] = Math.round(r + (newR - r) * strength);
|
||||
data[i + 1] = Math.round(g + (newG - g) * strength);
|
||||
data[i + 2] = Math.round(b + (newB - b) * strength);
|
||||
}
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
save_invert(params) {
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var data = this.apply_invert(img, params);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
|
||||
return app.State.do_action(
|
||||
new app.Actions.Update_layer_image_action(canvas)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust brightness/contrast only in the selected area
|
||||
*/
|
||||
adjust_selection() {
|
||||
var _this = this;
|
||||
|
||||
if (!this.hasSelection()) {
|
||||
alertify.error('No selection. Use a selection tool first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please select an image layer');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Adjust Selection',
|
||||
preview: true,
|
||||
params: [
|
||||
{name: "brightness", title: "Brightness:", value: 0, range: [-100, 100]},
|
||||
{name: "contrast", title: "Contrast:", value: 0, range: [-100, 100]},
|
||||
{name: "gamma", title: "Gamma:", value: 1, range: [0.1, 3], step: 0.1},
|
||||
],
|
||||
on_change: function (params, canvas_preview, w, h) {
|
||||
var img = canvas_preview.getImageData(0, 0, w, h);
|
||||
var data = _this.apply_adjust(img, params);
|
||||
canvas_preview.putImageData(data, 0, 0);
|
||||
},
|
||||
on_finish: function (params) {
|
||||
_this.save_adjust(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
apply_adjust(imageData, params) {
|
||||
if (!this.hasSelection()) return imageData;
|
||||
|
||||
var data = imageData.data;
|
||||
var width = imageData.width;
|
||||
var height = imageData.height;
|
||||
var brightness = (params.brightness || 0) * 2.55;
|
||||
var contrast = (params.contrast || 0) / 100;
|
||||
var gamma = params.gamma || 1;
|
||||
|
||||
var factor = (1 + contrast);
|
||||
|
||||
// Get mask data
|
||||
var maskCanvas = window.smartSelectMask.canvas;
|
||||
var scaledMask = document.createElement('canvas');
|
||||
scaledMask.width = width;
|
||||
scaledMask.height = height;
|
||||
var scaledCtx = scaledMask.getContext('2d');
|
||||
scaledCtx.drawImage(maskCanvas, 0, 0, width, height);
|
||||
var maskData = scaledCtx.getImageData(0, 0, width, height).data;
|
||||
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
var maskValue = maskData[i] / 255;
|
||||
|
||||
if (maskValue > 0.5) {
|
||||
for (var c = 0; c < 3; c++) {
|
||||
var value = data[i + c];
|
||||
|
||||
// Apply brightness
|
||||
value += brightness;
|
||||
|
||||
// Apply contrast
|
||||
value = ((value - 128) * factor) + 128;
|
||||
|
||||
// Apply gamma
|
||||
value = 255 * Math.pow(value / 255, 1 / gamma);
|
||||
|
||||
data[i + c] = Math.max(0, Math.min(255, Math.round(value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
save_adjust(params) {
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var data = this.apply_adjust(img, params);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
|
||||
return app.State.do_action(
|
||||
new app.Actions.Update_layer_image_action(canvas)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert selection to greyscale (useful for depth maps)
|
||||
*/
|
||||
greyscale_selection() {
|
||||
var _this = this;
|
||||
|
||||
if (!this.hasSelection()) {
|
||||
alertify.error('No selection. Use a selection tool first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please select an image layer');
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = {
|
||||
title: 'Greyscale Selection',
|
||||
preview: true,
|
||||
params: [
|
||||
{
|
||||
name: "method",
|
||||
title: "Method:",
|
||||
values: ["Luminosity", "Average", "Lightness"],
|
||||
value: "Luminosity"
|
||||
},
|
||||
{name: "invert", title: "Invert:", value: false},
|
||||
],
|
||||
on_change: function (params, canvas_preview, w, h) {
|
||||
var img = canvas_preview.getImageData(0, 0, w, h);
|
||||
var data = _this.apply_greyscale_selection(img, params);
|
||||
canvas_preview.putImageData(data, 0, 0);
|
||||
},
|
||||
on_finish: function (params) {
|
||||
_this.save_greyscale_selection(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
apply_greyscale_selection(imageData, params) {
|
||||
if (!this.hasSelection()) return imageData;
|
||||
|
||||
var data = imageData.data;
|
||||
var width = imageData.width;
|
||||
var height = imageData.height;
|
||||
var method = params.method || "Luminosity";
|
||||
var invert = params.invert || false;
|
||||
|
||||
var maskCanvas = window.smartSelectMask.canvas;
|
||||
var scaledMask = document.createElement('canvas');
|
||||
scaledMask.width = width;
|
||||
scaledMask.height = height;
|
||||
var scaledCtx = scaledMask.getContext('2d');
|
||||
scaledCtx.drawImage(maskCanvas, 0, 0, width, height);
|
||||
var maskData = scaledCtx.getImageData(0, 0, width, height).data;
|
||||
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
var maskValue = maskData[i] / 255;
|
||||
|
||||
if (maskValue > 0.5) {
|
||||
var r = data[i];
|
||||
var g = data[i + 1];
|
||||
var b = data[i + 2];
|
||||
var grey;
|
||||
|
||||
switch (method) {
|
||||
case "Luminosity":
|
||||
grey = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
break;
|
||||
case "Average":
|
||||
grey = (r + g + b) / 3;
|
||||
break;
|
||||
case "Lightness":
|
||||
grey = (Math.max(r, g, b) + Math.min(r, g, b)) / 2;
|
||||
break;
|
||||
default:
|
||||
grey = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
if (invert) {
|
||||
grey = 255 - grey;
|
||||
}
|
||||
|
||||
grey = Math.max(0, Math.min(255, Math.round(grey)));
|
||||
|
||||
data[i] = grey;
|
||||
data[i + 1] = grey;
|
||||
data[i + 2] = grey;
|
||||
}
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
save_greyscale_selection(params) {
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var data = this.apply_greyscale_selection(img, params);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
|
||||
return app.State.do_action(
|
||||
new app.Actions.Update_layer_image_action(canvas)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Image_selection_effects_class;
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Layer Scale module - Scale individual layers (like GIMP's Scale Layer)
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Base_layers_class from './../../core/base-layers.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import Helper_class from './../../libs/helpers.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
import Pica from './../../../../node_modules/pica/dist/pica.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Layer_scale_class {
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
instance = this;
|
||||
|
||||
this.Base_layers = new Base_layers_class();
|
||||
this.POP = new Dialog_class();
|
||||
this.Helper = new Helper_class();
|
||||
this.pica = Pica();
|
||||
}
|
||||
|
||||
scale() {
|
||||
var _this = this;
|
||||
|
||||
if (config.layer.type != 'image') {
|
||||
alertify.error('Please convert layer to raster first (Layer > Raster)');
|
||||
return;
|
||||
}
|
||||
|
||||
var currentWidth = config.layer.width;
|
||||
var currentHeight = config.layer.height;
|
||||
var aspectRatio = currentWidth / currentHeight;
|
||||
|
||||
var settings = {
|
||||
title: 'Scale Layer',
|
||||
params: [
|
||||
{name: "width", title: "Width:", value: currentWidth, placeholder: currentWidth},
|
||||
{name: "height", title: "Height:", value: currentHeight, placeholder: currentHeight},
|
||||
{name: "width_percent", title: "Width %:", value: 100, placeholder: 100},
|
||||
{name: "height_percent", title: "Height %:", value: 100, placeholder: 100},
|
||||
{name: "maintain_aspect", title: "Maintain Aspect Ratio:", value: true},
|
||||
{name: "interpolation", title: "Interpolation:", values: ["Lanczos (Best)", "Bilinear", "Nearest"]},
|
||||
],
|
||||
on_change: function(params) {
|
||||
// Auto-adjust to maintain aspect ratio if enabled
|
||||
if (params.maintain_aspect) {
|
||||
var widthInput = document.getElementById("pop_data_width");
|
||||
var heightInput = document.getElementById("pop_data_height");
|
||||
var widthPercentInput = document.getElementById("pop_data_width_percent");
|
||||
var heightPercentInput = document.getElementById("pop_data_height_percent");
|
||||
|
||||
// This is simplified - in practice you'd track which field changed
|
||||
}
|
||||
},
|
||||
on_finish: function (params) {
|
||||
_this.do_scale(params);
|
||||
},
|
||||
};
|
||||
this.POP.show(settings);
|
||||
}
|
||||
|
||||
async do_scale(params) {
|
||||
var layer = config.layer;
|
||||
|
||||
if (layer.type != 'image') {
|
||||
alertify.error('Layer must be an image');
|
||||
return;
|
||||
}
|
||||
|
||||
var currentWidth = layer.width;
|
||||
var currentHeight = layer.height;
|
||||
|
||||
// Calculate new dimensions
|
||||
var newWidth, newHeight;
|
||||
|
||||
if (params.width && params.width != currentWidth) {
|
||||
newWidth = parseInt(params.width);
|
||||
if (params.maintain_aspect) {
|
||||
newHeight = Math.round(newWidth / (currentWidth / currentHeight));
|
||||
} else {
|
||||
newHeight = params.height ? parseInt(params.height) : currentHeight;
|
||||
}
|
||||
} else if (params.height && params.height != currentHeight) {
|
||||
newHeight = parseInt(params.height);
|
||||
if (params.maintain_aspect) {
|
||||
newWidth = Math.round(newHeight * (currentWidth / currentHeight));
|
||||
} else {
|
||||
newWidth = params.width ? parseInt(params.width) : currentWidth;
|
||||
}
|
||||
} else if (params.width_percent && params.width_percent != 100) {
|
||||
newWidth = Math.round(currentWidth * params.width_percent / 100);
|
||||
if (params.maintain_aspect) {
|
||||
newHeight = Math.round(currentHeight * params.width_percent / 100);
|
||||
} else {
|
||||
newHeight = Math.round(currentHeight * (params.height_percent || 100) / 100);
|
||||
}
|
||||
} else if (params.height_percent && params.height_percent != 100) {
|
||||
newHeight = Math.round(currentHeight * params.height_percent / 100);
|
||||
if (params.maintain_aspect) {
|
||||
newWidth = Math.round(currentWidth * params.height_percent / 100);
|
||||
} else {
|
||||
newWidth = Math.round(currentWidth * (params.width_percent || 100) / 100);
|
||||
}
|
||||
} else {
|
||||
newWidth = parseInt(params.width) || currentWidth;
|
||||
newHeight = parseInt(params.height) || currentHeight;
|
||||
}
|
||||
|
||||
if (newWidth <= 0 || newHeight <= 0) {
|
||||
alertify.error('Invalid dimensions');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newWidth === currentWidth && newHeight === currentHeight) {
|
||||
alertify.warning('No change in size');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get canvas from layer
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false);
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
// Create destination canvas
|
||||
var destCanvas = document.createElement('canvas');
|
||||
destCanvas.width = newWidth;
|
||||
destCanvas.height = newHeight;
|
||||
var destCtx = destCanvas.getContext('2d');
|
||||
|
||||
// Perform resize based on interpolation method
|
||||
if (params.interpolation === "Lanczos (Best)") {
|
||||
await this.pica.resize(canvas, destCanvas, { alpha: true });
|
||||
} else if (params.interpolation === "Bilinear") {
|
||||
destCtx.imageSmoothingEnabled = true;
|
||||
destCtx.imageSmoothingQuality = 'high';
|
||||
destCtx.drawImage(canvas, 0, 0, newWidth, newHeight);
|
||||
} else {
|
||||
// Nearest neighbor
|
||||
destCtx.imageSmoothingEnabled = false;
|
||||
destCtx.drawImage(canvas, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
// Calculate new position (keep center in same place)
|
||||
var centerX = layer.x + layer.width / 2;
|
||||
var centerY = layer.y + layer.height / 2;
|
||||
var newX = Math.round(centerX - newWidth / 2);
|
||||
var newY = Math.round(centerY - newHeight / 2);
|
||||
|
||||
// Apply the changes
|
||||
app.State.do_action(
|
||||
new app.Actions.Bundle_action('scale_layer', 'Scale Layer', [
|
||||
new app.Actions.Update_layer_image_action(destCanvas, layer.id),
|
||||
new app.Actions.Update_layer_action(layer.id, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
width_original: newWidth,
|
||||
height_original: newHeight
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
alertify.success('Layer scaled to ' + newWidth + 'x' + newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer_scale_class;
|
||||
Reference in New Issue
Block a user