paintplus: vendor the app source and rename from EditmaskwithAI

Bring the full EditmaskwithAI application into the repo under paintplus/
(429 files) so the service is self-contained — the installer copies the
vendored source to ~/docker/paintplus/src instead of cloning at runtime.

Rename to PaintPlus (service + branding; app logic untouched):
- services/editmaskwithai.sh -> services/paintplus.sh (register_service
  paintplus, install_paintplus, ~/docker/paintplus, Caddy paintplus:8000,
  Authelia option preserved)
- container names -> paintplus across docker-compose*.yml; dev network
  -> paintplus-network
- browser <title> -> "PaintPlus - AI Image Editor"; README heading ->
  PaintPlus with upstream provenance note
- README utilities table: editmaskwithai -> paintplus

Backend/frontend code (help strings referencing the old container name,
the ai_photo_edit.db filename) is intentionally left as-is to avoid
touching application logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
This commit is contained in:
Claude
2026-06-26 05:48:43 +00:00
parent b4e8ba2a79
commit 084922afaa
431 changed files with 87396 additions and 77 deletions
@@ -0,0 +1,114 @@
/**
* Layer Alignment — align the active layer (or multiple selected layers) to the canvas.
* Operations: center H, center V, center both, align left/right/top/bottom, distribute.
* Shows as a compact floating toolbar.
*
* Menu target: layer/align.align
*/
import app from './../../app.js';
import config from './../../config.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
const BUTTONS = [
{ id: 'ch', label: '⬌', title: 'Center horizontally on canvas' },
{ id: 'cv', label: '⬍', title: 'Center vertically on canvas' },
{ id: 'cc', label: '⊕', title: 'Center on canvas' },
{ id: 'sep', label: '|', title: '', sep: true },
{ id: 'al', label: '⇤', title: 'Align left edge to canvas' },
{ id: 'ar', label: '⇥', title: 'Align right edge to canvas' },
{ id: 'at', label: '⇡', title: 'Align top edge to canvas' },
{ id: 'ab', label: '⇣', title: 'Align bottom edge to canvas' },
];
class Layer_align_class {
constructor() {
if (instance) return instance;
instance = this;
this._panel = null;
}
align() {
if (this._panel) { this._removePanel(); return; }
this._mountPanel();
}
_mountPanel() {
this._removePanel();
const panel = document.createElement('div');
panel.id = 'align_panel';
Object.assign(panel.style, {
position: 'fixed',
top: '60px',
left: '50%',
transform: 'translateX(-50%)',
background: '#1a1a1a',
border: '1px solid #3a3a3a',
borderRadius: '10px',
padding: '7px 10px',
display: 'flex',
alignItems: 'center',
gap: '4px',
zIndex: '8889',
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
fontFamily: 'sans-serif',
userSelect: 'none',
});
const btnHtml = BUTTONS.map(b => {
if (b.sep) return `<span style="color:#444;padding:0 2px;">│</span>`;
return `<button data-align="${b.id}" title="${b.title}"
style="width:30px;height:30px;border-radius:6px;border:1px solid #444;
background:#252525;color:#ccc;cursor:pointer;font-size:16px;
display:flex;align-items:center;justify-content:center;
transition:background .12s;"
onmouseover="this.style.background='#333'"
onmouseout="this.style.background='#252525'">${b.label}</button>`;
}).join('');
panel.innerHTML = `
<span style="font-size:11px;color:#555;margin-right:4px;">Align:</span>
${btnHtml}
<span id="align-close" style="margin-left:6px;cursor:pointer;color:#555;font-size:18px;">×</span>`;
document.body.appendChild(panel);
this._panel = panel;
panel.querySelector('#align-close').addEventListener('click', () => this._removePanel());
panel.querySelectorAll('[data-align]').forEach(btn => {
btn.addEventListener('click', () => this._doAlign(btn.dataset.align));
});
}
_doAlign(op) {
const layer = config.layer;
if (!layer) { alertify.error('Select a layer first.'); return; }
const cw = config.WIDTH;
const ch = config.HEIGHT;
const lw = layer.width;
const lh = layer.height;
let newX = layer.x;
let newY = layer.y;
if (op === 'ch' || op === 'cc') newX = Math.round((cw - lw) / 2);
if (op === 'cv' || op === 'cc') newY = Math.round((ch - lh) / 2);
if (op === 'al') newX = 0;
if (op === 'ar') newX = cw - lw;
if (op === 'at') newY = 0;
if (op === 'ab') newY = ch - lh;
app.State.do_action(
new app.Actions.Update_layer_action(layer.id, { x: newX, y: newY })
);
}
_removePanel() {
if (this._panel) { this._panel.remove(); this._panel = null; }
}
}
export default Layer_align_class;
@@ -0,0 +1,19 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
class Layer_clear_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
clear() {
return app.State.do_action(
new app.Actions.Clear_layer_action(config.layer.id)
);
}
}
export default Layer_clear_class;
@@ -0,0 +1,86 @@
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import Base_gui_class from "../../core/base-gui.js";
class Layer_composition_class {
constructor() {
this.POP = new Dialog_class();
this.Base_gui_class = new Base_gui_class();
}
composition() {
var compositions = [
"-- Default --",
"color",
"color-burn",
"color-dodge",
"copy",
"darken",
"darker",
"destination-atop",
"destination-in",
"destination-out",
"destination-over",
"difference",
"exclusion",
"hard-light",
"hue",
"lighten",
"lighter",
"luminosity",
"multiply",
"overlay",
"saturation",
"screen",
"soft-light",
"source-atop",
"source-in",
"source-out",
"source-over",
"xor",
];
var initial_composition = config.layer.composition;
var _this = this;
var settings = {
title: 'Composition',
//preview: true,
params: [
{name: "composition", title: "Composition:", value: config.layer.composition, values: compositions},
],
on_change: function (params, canvas_preview, w, h) {
//redraw preview
if (params.composition == '-- Default --') {
params.composition = 'source-over';
}
config.layer.composition = params.composition;
config.need_render = true;
_this.Base_gui_class.GUI_layers.render_layers();
},
on_finish: function (params) {
config.layer.composition = initial_composition;
if (params.composition == '-- Default --') {
params.composition = 'source-over';
}
app.State.do_action(
new app.Actions.Bundle_action('change_composition', 'Change Composition', [
new app.Actions.Update_layer_action(config.layer.id, {
composition: params.composition
})
])
);
},
on_cancel: function (params) {
config.layer.composition = initial_composition;
config.need_render = true;
_this.Base_gui_class.GUI_layers.render_layers();
},
};
this.POP.show(settings);
}
}
export default Layer_composition_class;
@@ -0,0 +1,19 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
class Layer_delete_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
delete() {
app.State.do_action(
new app.Actions.Delete_layer_action(config.layer.id)
);
}
}
export default Layer_delete_class;
@@ -0,0 +1,105 @@
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 alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Layer_differences_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
differences() {
var _this = this;
if (this.Base_layers.find_previous(config.layer.id) == null) {
alertify.error('There are no layers behind.');
return false;
}
var settings = {
title: 'Differences',
preview: true,
params: [
{name: "sensitivity", title: "Sensitivity:", value: "0", range: [0, 255]},
],
on_change: function (params, canvas_preview, w, h) {
_this.calc_differences(params.sensitivity, canvas_preview, w, h);
},
on_finish: function (params) {
_this.calc_differences(params.sensitivity);
},
};
this.POP.show(settings);
}
calc_differences(sensitivity, canvas_preview, w, h) {
//create tmp canvas
var canvas = document.createElement('canvas');
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
var ctx = canvas.getContext("2d");
//get source data
this.Base_layers.render_object(ctx, config.layer);
var imgData1 = ctx.getImageData(0, 0, config.WIDTH, config.HEIGHT).data;
ctx.clearRect(0, 0, canvas.width, canvas.height);
//get target data
var next_layer = this.Base_layers.find_previous(config.layer.id);
this.Base_layers.render_object(ctx, next_layer);
var imgData2 = ctx.getImageData(0, 0, config.WIDTH, config.HEIGHT).data;
//prepare background
ctx.rect(0, 0, config.WIDTH, config.HEIGHT);
ctx.fillStyle = "#ffffff";
ctx.fill();
//generate diff
var img3 = ctx.getImageData(0, 0, config.WIDTH, config.HEIGHT);
var imgData3 = img3.data;
for (var xx = 0; xx < config.WIDTH; xx++) {
for (var yy = 0; yy < config.HEIGHT; yy++) {
var x = (xx + yy * config.WIDTH) * 4;
if (Math.abs(imgData1[x] - imgData2[x]) > sensitivity
|| Math.abs(imgData1[x + 1] - imgData2[x + 1]) > sensitivity
|| Math.abs(imgData1[x + 2] - imgData2[x + 2]) > sensitivity
|| Math.abs(imgData1[x + 3] - imgData2[x + 3]) > sensitivity) {
imgData3[x] = 255;
imgData3[x + 1] = 0;
imgData3[x + 2] = 0;
imgData3[x + 3] = 255;
}
}
}
ctx.putImageData(img3, 0, 0);
//show
if (canvas_preview == undefined) {
//main
var params = [];
params.type = 'image';
params.name = 'Differences';
params.data = canvas.toDataURL("image/png");
app.State.do_action(
new app.Actions.Insert_layer_action(params)
);
}
else {
//preview
canvas_preview.save();
canvas_preview.scale(w / config.WIDTH, h / config.HEIGHT);
canvas_preview.drawImage(canvas, 0, 0);
canvas_preview.restore();
}
canvas.width = 1;
canvas.height = 1;
}
}
export default Layer_differences_class;
@@ -0,0 +1,78 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import Helper_class from './../../libs/helpers.js';
var instance = null;
class Layer_duplicate_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.keyCode;
if (this.Helper.is_input(event.target))
return;
if (code == 68) {
//D - duplicate
this.duplicate();
event.preventDefault();
}
}, false);
}
duplicate() {
var params = JSON.parse(JSON.stringify(config.layer));
delete params.id;
delete params.order;
//generate name
var name_number = params.name.match(/^(.*) #([0-9]+)$/);
if(name_number == null){
//first duplicate
params.name = params.name + " #2";
}
else{
//nth duplicate - name like "query #17"
params.name = name_number[1] + " #" + (parseInt(name_number[2]) + 1)
}
if(params.x != 0 || params.y != 0 || params.width != config.WIDTH || params.height != config.HEIGHT){
params.x += 10;
params.y += 10;
}
for (var i in params) {
//remove private attributes
if (i[0] == '_')
delete params[i];
}
if (params.type == 'image') {
//image
params.link = config.layer.link.cloneNode(true);
}
app.State.do_action(
new app.Actions.Bundle_action('duplicate_layer', 'Duplicate Layer', [
new app.Actions.Insert_layer_action(params)
])
);
}
}
export default Layer_duplicate_class;
@@ -0,0 +1,56 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Layer_flatten_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
flatten() {
//create tmp canvas
var canvas = document.createElement('canvas');
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
var ctx = canvas.getContext("2d");
var layers_sorted = this.Base_layers.get_sorted_layers();
//paint layers
for (var i = layers_sorted.length - 1; i >= 0; i--) {
var layer = layers_sorted[i];
ctx.globalAlpha = layer.opacity / 100;
ctx.globalCompositeOperation = layer.composition;
this.Base_layers.render_object(ctx, layer);
}
//create requested layer
var params = [];
params.type = 'image';
params.name = 'Merged';
params.data = canvas.toDataURL("image/png");
//remove rest of layers
let delete_actions = [];
for (var i = config.layers.length - 1; i >= 0; i--) {
delete_actions.push(new app.Actions.Delete_layer_action(config.layers[i].id));
}
// Run actions
app.State.do_action(
new app.Actions.Bundle_action('flatten_image', 'Flatten Image', [
new app.Actions.Insert_layer_action(params),
...delete_actions
])
);
canvas.width = 1;
canvas.height = 1;
}
}
export default Layer_flatten_class;
@@ -0,0 +1,59 @@
import app from './../../app.js';
import config from './../../config.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import Base_layers_class from './../../core/base-layers.js';
class Layer_merge_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
merge() {
if (this.Base_layers.find_previous(config.layer.id) == null) {
alertify.error('There are no layers behind.');
return false;
}
//create tmp canvas
var canvas = document.createElement('canvas');
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
var ctx = canvas.getContext("2d");
//first layer
var previous_layer = this.Base_layers.find_previous(config.layer.id);
var previous_id = previous_layer.id;
ctx.globalAlpha = previous_layer.opacity / 100;
ctx.globalCompositeOperation = previous_layer.composition;
this.Base_layers.render_object(ctx, previous_layer);
//second layer
var current_id = config.layer.id;
var current_order = config.layer.order;
ctx.globalAlpha = config.layer.opacity / 100;
ctx.globalCompositeOperation = config.layer.composition;
this.Base_layers.render_object(ctx, config.layer);
//create requested layer
var params = [];
params.type = 'image';
params.name = config.layer.name + ' + merged';
params.order = current_order;
params.data = canvas.toDataURL("image/png");
app.State.do_action(
new app.Actions.Bundle_action('merge_layers', 'Merge Layers', [
new app.Actions.Insert_layer_action(params),
new app.Actions.Delete_layer_action(current_id),
new app.Actions.Delete_layer_action(previous_id)
])
);
//free canvas data
canvas.width = 1;
canvas.height = 1;
}
}
export default Layer_merge_class;
@@ -0,0 +1,24 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
class Layer_move_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
up() {
app.State.do_action(
new app.Actions.Reorder_layer_action(config.layer.id, 1)
);
}
down() {
app.State.do_action(
new app.Actions.Reorder_layer_action(config.layer.id, -1)
);
}
}
export default Layer_move_class;
@@ -0,0 +1,97 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import GUI_tools_class from './../../core/gui/gui-tools.js';
import Base_selection_class from './../../core/base-selection.js';
import Selection_class from './../../tools/selection.js';
import Helper_class from './../../libs/helpers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Layer_new_class {
constructor() {
this.Base_layers = new Base_layers_class();
this.Selection = new Selection_class();
this.Base_selection = new Base_selection_class(this.Base_layers.ctx);
this.GUI_tools = new GUI_tools_class();
this.Helper = new Helper_class();
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.keyCode;
if (this.Helper.is_input(event.target))
return;
if (code == 78 && event.ctrlKey != true && event.metaKey != true) {
//N
this.new();
}
}, false);
}
new() {
app.State.do_action(
new app.Actions.Insert_layer_action()
);
}
new_selection() {
var selection = this.Base_selection.get_selection();
var layer = config.layer;
if (selection.width === null || config.layer.type != 'image') {
alertify.error('Empty selection or type not image.');
return;
}
if (config.TOOL.name != 'selection') {
alertify.error('Empty selection or type not image.');
return;
}
//if image was stretched
var width_ratio = (layer.width / layer.width_original);
var height_ratio = (layer.height / layer.height_original);
var left = selection.x - layer.x;
var top = selection.y - layer.y;
//adapt to origin size
selection.width = selection.width / width_ratio;
selection.height = selection.height / height_ratio;
//create new layer
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = Math.round(selection.width);
canvas.height = Math.round(selection.height);
ctx.translate(-left / width_ratio, -top / height_ratio);
ctx.drawImage(config.layer.link, 0, 0);
ctx.translate(0, 0);
//register it
var params = {
x: Math.round(selection.x),
y: Math.round(selection.y),
width: Math.round(selection.width * width_ratio),
height: Math.round(selection.height * height_ratio),
width_original: Math.round(selection.width),
height_original: Math.round(selection.height),
type: 'image',
data: canvas.toDataURL("image/png"),
};
app.State.do_action(
new app.Actions.Bundle_action('new_layer', 'New Layer', [
new app.Actions.Insert_layer_action(params, false),
...this.Selection.on_leave(),
new app.Actions.Activate_tool_action('select')
])
);
}
}
export default Layer_new_class;
@@ -0,0 +1,38 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Layer_raster_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
raster() {
var canvas = this.Base_layers.convert_layer_to_canvas();
var current_layer = config.layer;
var current_id = current_layer.id;
//show
var params = {
type: 'image',
name: config.layer.name + ' + raster',
data: canvas.toDataURL("image/png"),
x: parseInt(canvas.dataset.x),
y: parseInt(canvas.dataset.y),
width: canvas.width,
height: canvas.height,
opacity: current_layer.opacity,
};
app.State.do_action(
new app.Actions.Bundle_action('convert_to_raster', 'Convert to Raster', [
new app.Actions.Insert_layer_action(params, false),
new app.Actions.Delete_layer_action(current_id)
])
);
}
}
export default Layer_raster_class;
@@ -0,0 +1,55 @@
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';
class Layer_rename_class {
constructor() {
this.Base_layers = new Base_layers_class();
this.POP = new Dialog_class();
this.Helper = new Helper_class();
}
rename(id = null) {
var _this = this;
var name_ = this.Helper.escapeHtml(config.layer.name);
var settings = {
title: 'Rename',
params: [
{name: "name", title: "Name:", value: name_},
],
on_load: function () {
document.querySelector('#pop_data_name').select();
},
on_finish: function (params) {
app.State.do_action(
new app.Actions.Bundle_action('rename_layer', 'Rename Layer', [
new app.Actions.Refresh_layers_gui_action('undo'),
new app.Actions.Update_layer_action(id || config.layer.id, {
name: _this.validate_name(params.name)
}),
new app.Actions.Refresh_layers_gui_action('do')
])
);
},
};
this.POP.show(settings);
}
validate_name(text) {
text = text
.replace(/&/g, "-")
.replace(/</g, "-")
.replace(/>/g, "-")
.replace(/"/g, "-")
.replace(/'/g, "-");
return text;
}
}
export default Layer_rename_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;
@@ -0,0 +1,19 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
class Layer_visibility_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
toggle() {
app.State.do_action(
new app.Actions.Toggle_layer_visibility_action(config.layer.id)
);
}
}
export default Layer_visibility_class;