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:
@@ -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;
|
||||
Reference in New Issue
Block a user