Add layer scaling, greyscale effect, and context menu
- Fix onnxruntime version (1.15.1) to avoid executable stack issue - Add Layer Scale module (Layer > Scale Layer or 'S' button) - Add Greyscale effect with multiple methods (luminosity, average, etc.) - Add right-click context menu on layers with common operations - Add Scale button to layers panel toolbar
This commit is contained in:
@@ -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,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