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,82 @@
import config from "../../config";
import Base_layers_class from './../../core/base-layers.js';
import File_save_class from './../file/save.js';
import Helper_class from './../../libs/helpers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
class Copy_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.File_save = new File_save_class();
//events
document.addEventListener('keydown', (event) => {
var code = event.key.toLowerCase();
var ctrlDown = event.ctrlKey || event.metaKey;
if (this.Helper.is_input(event.target))
return;
if (code == "c" && ctrlDown == true) {
//copy to clipboard
this.copy_to_clipboard();
}
}, false);
}
async copy_to_clipboard(){
var _this = this;
const canWriteToClipboard = await this.askWritePermission();
if (canWriteToClipboard) {
//get data - current layer
var canvas = this.Base_layers.convert_layer_to_canvas();
var ctx = canvas.getContext("2d");
if (config.TRANSPARENCY == false) {
//add white background
ctx.globalCompositeOperation = 'destination-over';
this.File_save.fillCanvasBackground(ctx, '#ffffff');
ctx.globalCompositeOperation = 'source-over';
}
//save using lib
canvas.toBlob(function (blob) {
_this.setToClipboard(blob);
});
}
else{
alertify.error('Missing permissions to write to Clipboard.cc');
}
}
async setToClipboard(blob) {
const data = [new ClipboardItem({ [blob.type]: blob })];
await navigator.clipboard.write(data);
}
async askWritePermission() {
try {
// The clipboard-write permission is granted automatically to pages
// when they are the active tab. So it's not required, but it's more safe.
const { state } = await navigator.permissions.query({ name: 'clipboard-write' })
return state === 'granted';
}
catch (error) {
// Browser compatibility / Security error (ONLY HTTPS) ...
return false;
}
}
}
export default Copy_class;
@@ -0,0 +1,145 @@
/**
* History Panel — visual undo history timeline.
* Shows the last N actions as a clickable list. Click any item to undo/redo to that point.
* Docks as a floating panel on the right side of the screen.
*
* Menu target: edit/history_panel.toggle
*/
import app from './../../app.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
class Edit_history_panel_class {
constructor() {
if (instance) return instance;
instance = this;
this._panel = null;
this._interval = null;
}
toggle() {
if (this._panel) {
this._stop();
} else {
this._start();
}
}
_start() {
this._buildPanel();
this._render();
// Refresh whenever the history changes (poll lightly)
this._interval = setInterval(() => this._render(), 800);
}
_stop() {
if (this._interval) { clearInterval(this._interval); this._interval = null; }
if (this._panel) { this._panel.remove(); this._panel = null; }
}
_buildPanel() {
const panel = document.createElement('div');
panel.id = 'history_panel';
Object.assign(panel.style, {
position: 'fixed',
top: '60px',
right: '0',
width: '200px',
maxHeight: 'calc(100vh - 80px)',
overflowY: 'auto',
background: '#1a1a1a',
borderLeft: '1px solid #333',
borderBottom: '1px solid #333',
borderRadius: '0 0 0 10px',
zIndex: '8888',
fontFamily: 'sans-serif',
fontSize: '12px',
color: '#ccc',
boxShadow: '-4px 4px 16px rgba(0,0,0,0.4)',
userSelect: 'none',
});
panel.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;
padding:8px 10px;border-bottom:1px solid #333;position:sticky;top:0;
background:#1a1a1a;z-index:1;">
<span style="font-size:12px;color:#888;font-weight:600;">History</span>
<span id="hist-close" style="cursor:pointer;color:#555;font-size:16px;">×</span>
</div>
<div id="hist-list"></div>`;
document.body.appendChild(panel);
this._panel = panel;
panel.querySelector('#hist-close').addEventListener('click', () => this._stop());
}
_render() {
if (!this._panel) return;
const list = this._panel.querySelector('#hist-list');
if (!list) return;
const history = app.State.action_history || [];
const idx = app.State.action_history_index ?? history.length;
if (history.length === 0) {
list.innerHTML = `<div style="padding:12px 10px;color:#555;">No actions yet.</div>`;
return;
}
// Build rows newest-first
const rows = [];
// "Current state" row at top
const atTop = idx >= history.length;
rows.push(`<div data-idx="${history.length}"
style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #222;
background:${atTop ? '#1e3a5f' : 'transparent'};
color:${atTop ? '#93c5fd' : '#666'};"
>
<span style="margin-right:6px;font-size:10px;">${atTop ? '▶' : '○'}</span>Current state
</div>`);
for (let i = history.length - 1; i >= 0; i--) {
const action = history[i];
const isCurrent = (i === idx - 1);
const isFuture = (i >= idx);
const label = action.action_description || action.action_id || `Step ${i + 1}`;
rows.push(`<div data-idx="${i}"
style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #1e1e1e;
background:${isCurrent ? '#1e3a5f' : 'transparent'};
color:${isFuture ? '#444' : isCurrent ? '#93c5fd' : '#ccc'};"
>
<span style="margin-right:6px;font-size:10px;">${isCurrent ? '▶' : isFuture ? '○' : '·'}</span>${_escHtml(label)}
</div>`);
}
list.innerHTML = rows.join('');
// Wire clicks
list.querySelectorAll('[data-idx]').forEach(el => {
el.addEventListener('click', () => {
const target = parseInt(el.dataset.idx, 10);
this._jumpTo(target);
});
});
}
_jumpTo(targetIdx) {
const history = app.State.action_history || [];
const current = app.State.action_history_index ?? history.length;
if (targetIdx === current) return;
const steps = targetIdx - current;
if (steps > 0) {
for (let i = 0; i < steps; i++) app.State.redo_action();
} else {
for (let i = 0; i < Math.abs(steps); i++) app.State.undo_action();
}
this._render();
}
}
function _escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
export default Edit_history_panel_class;
@@ -0,0 +1,10 @@
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Edit_paste_class {
paste() {
alertify.error('Use Ctrl+V keyboard shortcut to paste from Clipboard.');
}
}
export default Edit_paste_class;
@@ -0,0 +1,14 @@
import Base_state_class from './../../core/base-state.js';
class Edit_redo_class {
constructor() {
this.Base_state = new Base_state_class();
}
redo() {
this.Base_state.redo();
}
}
export default Edit_redo_class;
@@ -0,0 +1,26 @@
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import Selection_class from './../../tools/selection.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Edit_selection_class {
constructor() {
this.Base_layers = new Base_layers_class();
this.Selection = new Selection_class(this.Base_layers.ctx);
}
select_all() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
this.Selection.select_all();
}
delete() {
this.Selection.delete_selection();
}
}
export default Edit_selection_class;
@@ -0,0 +1,31 @@
import Base_state_class from './../../core/base-state.js';
var instance = null;
class Edit_undo_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_state = new Base_state_class();
this.events();
}
events(){
var _this = this;
document.querySelector('#undo_button').addEventListener('click', function (event) {
_this.Base_state.undo();
});
}
undo() {
this.Base_state.undo();
}
}
export default Edit_undo_class;
@@ -0,0 +1,71 @@
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 Helper_class from './../../../libs/helpers.js';
class Effects_common_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.params = null;
}
show_dialog(type, params, filter_id) {
var _this = this;
var title = this.Helper.ucfirst(type);
title = title.replace(/-/g, ' ');
var preview_padding = 0;
if(typeof this.preview_padding != "undefined"){
preview_padding = this.preview_padding;
}
var settings = {
title: title,
preview: true,
preview_padding: preview_padding,
effects: true,
params: params,
on_change: function (params, canvas_preview, w, h) {
_this.params = params;
canvas_preview.filter = _this.preview(params, type);
canvas_preview.drawImage(this.layer_active_small,
preview_padding, preview_padding,
_this.POP.width_mini - preview_padding * 2, _this.POP.height_mini - preview_padding * 2
);
},
on_finish: function (params) {
_this.params = params;
_this.save(params, type, filter_id);
},
};
this.Base_layers.disable_filter(filter_id);
this.POP.show(settings);
this.Base_layers.disable_filter(null);
}
save(params, type, filter_id) {
return app.State.do_action(
new app.Actions.Add_layer_filter_action(null, type, params, filter_id)
);
}
preview(params, type) {
if(type == 'shadow'){
type = 'drop-shadow';
}
var value = this.convert_value(params.value, params, 'preview');
return type + "(" + value + ")";
}
convert_value(value, params) {
return value;
}
}
export default Effects_common_class;
@@ -0,0 +1,219 @@
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 Helper_class from './../../libs/helpers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_backAndWhite_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
}
black_and_white() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//create tmp canvas
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//calc default level
var default_level = this.thresholding(ctx, canvas.width, canvas.height, true);
var settings = {
title: 'Black and White',
preview: true,
effects: true,
params: [
{name: "level", title: "Level:", value: default_level, range: [0, 255]},
{name: "dithering", title: "Dithering:", value: false},
],
on_change: function (params, canvas_preview, w, h) {
//check params
var level = document.getElementById("pop_data_level");
if (params.dithering == false)
level.disabled = false;
else
level.disabled = true;
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var W = data.width;
var H = data.height;
//create tmp canvas
var canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
var imgData = data.data;
var grey, c, quant_error, m;
if (params.dithering !== true) {
//no differing
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
grey = Math.round(0.2126 * imgData[i] + 0.7152 * imgData[i + 1] + 0.0722 * imgData[i + 2]);
if (grey <= params.level)
c = 0;
else
c = 255;
imgData[i] = c;
imgData[i + 1] = c;
imgData[i + 2] = c;
}
}
else {
//FloydSteinberg dithering
var img2 = canvas.getContext("2d").getImageData(0, 0, W, H);
var imgData2 = img2.data;
for (var j = 0; j < H; j++) {
for (var i = 0; i < W; i++) {
var k = ((j * (W * 4)) + (i * 4));
if (imgData[k + 3] == 0)
continue; //transparent
grey = Math.round(0.2126 * imgData[k] + 0.7152 * imgData[k + 1] + 0.0722 * imgData[k + 2]);
grey = grey + imgData2[k]; //add data shft from previous iterations
c = Math.floor(grey / 256);
if (c == 1)
c = 255;
imgData[k] = c;
imgData[k + 1] = c;
imgData[k + 2] = c;
quant_error = grey - c;
if (i + 1 < W) {
m = k + 4;
imgData2[m] += Math.round(quant_error * 7 / 16);
}
if (i - 1 > 0 && j + 1 < H) {
m = k - 4 + W * 4;
imgData2[m] += Math.round(quant_error * 3 / 16);
}
if (j + 1 < H) {
m = k + W * 4;
imgData2[m] += Math.round(quant_error * 5 / 16);
}
if (i + 1 < W && j + 1 < H) {
m = k + 4 + W * 4;
imgData2[m] += Math.round(quant_error * 1 / 16);
}
}
}
}
return data;
}
thresholding(ctx, W, H, only_level) {
var img = ctx.getImageData(0, 0, W, H);
var imgData = img.data;
var hist_data = [];
var grey;
for (var i = 0; i <= 255; i++)
hist_data[i] = 0;
for (var i = 0; i < imgData.length; i += 4) {
grey = Math.round(0.2126 * imgData[i] + 0.7152 * imgData[i + 1] + 0.0722 * imgData[i + 2]);
hist_data[grey]++;
}
var level = this.otsu(hist_data, W * H);
if (only_level === true)
return level;
var c;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
grey = Math.round(0.2126 * imgData[i] + 0.7152 * imgData[i + 1] + 0.0722 * imgData[i + 2]);
if (grey < level)
c = 0;
else
c = 255;
imgData[i] = c;
imgData[i + 1] = c;
imgData[i + 2] = c;
}
ctx.putImageData(img, 0, 0);
}
//http://en.wikipedia.org/wiki/Otsu%27s_Method
otsu(histogram, total) {
var sum = 0;
for (var i = 1; i < 256; ++i)
sum += i * histogram[i];
var mB, mF, between;
var sumB = 0;
var wB = 0;
var wF = 0;
var max = 0;
var threshold = 0;
for (var i = 0; i < 256; ++i) {
wB += histogram[i];
if (wB == 0)
continue;
wF = total - wB;
if (wF == 0)
break;
sumB += i * histogram[i];
mB = sumB / wB;
mF = (sum - sumB) / wF;
between = wB * wF * Math.pow(mB - mF, 2);
if (between > max) {
max = between;
threshold = i;
}
}
return threshold;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var default_level = this.thresholding(ctx, canvas_thumb.width, canvas_thumb.height, true);
var params = {
level: default_level,
dithering: false,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_backAndWhite_class;
@@ -0,0 +1,157 @@
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 ImageFilters from './../../libs/imagefilters.js';
import glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_blueprint_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.ImageFilters = ImageFilters;
this.fx_filter = false;
}
blueprint() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var ctx = canvas.getContext("2d");
//create blue layer
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.fillStyle = '#0e58a3';
ctx2.fillRect(0, 0, width, height);
//apply edges
var img = ctx.getImageData(0, 0, width, height);
var img = this.ImageFilters.Edge(img);
ctx.putImageData(img, 0, 0);
//denoise
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).denoise(20).update(); //effect
canvas = this.fx_filter;
//Brightness
var img = ctx.getImageData(0, 0, width, height);
var img = this.ImageFilters.BrightnessContrastPhotoshop(img, 80, 0);
ctx.putImageData(img, 0, 0);
//merge
ctx2.globalCompositeOperation = "screen";
ctx2.filter = 'grayscale(1)';
ctx2.drawImage(canvas, 0, 0);
ctx2.globalCompositeOperation = "source-over";
ctx2.filter = 'none';
//draw lines
this.draw_grid(ctx2, 20);
return canvas2;
}
/**
* draw grid
*
* @param {CanvasContext} ctx
* @param {Int} size
*/
draw_grid(ctx, size) {
if (this.grid == false)
return;
var width = config.WIDTH;
var height = config.HEIGHT;
var color_main = 'rgba(255, 255, 255, 0.5)';
var color_small = 'rgba(255, 255, 255, 0.1)';
//size
if (size != undefined && size != undefined)
this.grid_size = [size, size];
else {
size = this.grid_size[0];
size = this.grid_size[1];
}
size = parseInt(size);
size = parseInt(size);
ctx.lineWidth = 1;
ctx.beginPath();
if (size < 2)
size = 2;
if (size < 2)
size = 2;
for (var i = size; i < width; i = i + size) {
if (size == 0)
break;
if (i % (size * 5) == 0) {
//main lines
ctx.strokeStyle = color_main;
}
else {
//small lines
ctx.strokeStyle = color_small;
}
ctx.beginPath();
ctx.moveTo(0.5 + i, 0);
ctx.lineTo(0.5 + i, height);
ctx.stroke();
}
for (var i = size; i < height; i = i + size) {
if (size == 0)
break;
if (i % (size * 5) == 0) {
//main lines
ctx.strokeStyle = color_main;
}
else {
//small lines
ctx.strokeStyle = color_small;
}
ctx.beginPath();
ctx.moveTo(0, 0.5 + i);
ctx.lineTo(width, 0.5 + i);
ctx.stroke();
}
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var data = this.change(canvas, canvas_thumb.width, canvas_thumb.height);
ctx.drawImage(data, 0, 0);
}
}
export default Effects_blueprint_class;
@@ -0,0 +1,107 @@
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';
import Effects_browser_class from "./browser";
class Effects_borders_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Effects_browser = new Effects_browser_class();
}
borders(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var _this = this;
var filter = this.Base_layers.find_filter_by_id(filter_id, 'borders');
var settings = {
title: 'Borders',
params: [
{name: "color", title: "Color:", value: filter.color ??= config.COLOR, type: 'color'},
{name: "size", title: "Size:", value: filter.size ??= 10},
],
on_finish: function (params) {
var target = Math.min(config.WIDTH, config.HEIGHT);
_this.add_borders(params, filter_id);
},
};
var rotate = config.layer.rotate;
config.layer.rotate = 0;
this.Base_layers.disable_filter(filter_id);
this.POP.show(settings);
config.layer.rotate = rotate;
this.Base_layers.disable_filter(null);
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
ctx.drawImage(canvas_thumb,
5, 5,
this.Effects_browser.preview_width - 10, this.Effects_browser.preview_height - 10);
//add borders
ctx.strokeStyle = '#000000';
ctx.lineWidth = 10;
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.stroke();
}
render_pre(ctx, data) {
}
render_post(ctx, data, layer){
var size = Math.max(0, data.params.size);
var x = layer.x;
var y = layer.y;
var width = parseInt(layer.width);
var height = parseInt(layer.height);
//legacy check
if(x == null) x = 0;
if(y == null) y = 0;
if(!width) width = config.WIDTH;
if(!height) height = config.HEIGHT;
ctx.save();
//set styles
ctx.strokeStyle = data.params.color;
ctx.lineWidth = size;
//draw with rotation support
ctx.translate(layer.x + width / 2, layer.y + height / 2);
ctx.rotate(layer.rotate * Math.PI / 180);
var x_new = -width / 2;
var y_new = -height / 2;
ctx.beginPath();
ctx.rect(x_new - size * 0.5, y_new - size * 0.5, width + size, height + size);
ctx.stroke();
ctx.restore();
}
add_borders(params, filter_id) {
//apply effect
return app.State.do_action(
new app.Actions.Add_layer_filter_action(config.layer.id, 'borders', params, filter_id)
);
}
}
export default Effects_borders_class;
@@ -0,0 +1,88 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_boxBlur_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
box_blur() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Box blur',
preview: true,
effects: true,
params: [
{name: "param1", title: "H Radius:", value: 3, range: [1, 20]},
{name: "param2", title: "V Radius:", value: 3, range: [1, 20]},
{name: "param3", title: "Quality:", value: 3, range: [1, 20]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var param1 = params.param1;
var param2 = params.param2;
var param3 = params.param3;
var filtered = ImageFilters.BoxBlur(data, param1, param2, param3);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
param1: 20,
param2: 1,
param3: 1,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_boxBlur_class;
@@ -0,0 +1,140 @@
import config from './../../config.js';
import Base_tools_class from './../../core/base-tools.js';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_browser_class extends Base_tools_class {
constructor(ctx) {
super();
this.POP = new Dialog_class();
this.preview_width = 150;
this.preview_height = 120;
}
async browser() {
var _this = this;
var html = '';
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var data = this.get_effects_list();
for (var i in data) {
var title = data[i].title;
html += '<div class="item">';
html += ' <canvas id="c_' + data[i].key + '" width="' + this.preview_width + '" height="'
+ this.preview_height + '" class="effectsPreview" data-key="'
+ data[i].key + '"></canvas>';
html += '<div class="preview-item-title">' + title + '</div>';
html += '</div>';
}
for (var i = 0; i < 4; i++) {
html += '<div class="item"></div>';
}
var settings = {
title: 'Effects browser',
className: 'wide',
on_load: function (params, popup) {
var node = document.createElement("div");
node.classList.add('flex-container');
node.innerHTML = html;
popup.el.querySelector('.dialog_content').appendChild(node);
//events
var targets = popup.el.querySelectorAll('.item canvas');
for (var i = 0; i < targets.length; i++) {
targets[i].addEventListener('click', function (event) {
//we have click
var key = this.dataset.key;
for (var i in data) {
if(data[i].key == key){
var function_name = _this.get_function_from_path(key);
_this.POP.hide();
data[i].object[function_name]();
}
}
});
}
},
};
this.POP.show(settings);
//sleep, lets wait till DOM is finished
await new Promise(r => setTimeout(r, 10));
//generate thumb
var active_image = this.Base_layers.convert_layer_to_canvas();
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = this.preview_width;
canvas.height = this.preview_height;
ctx.scale(this.preview_width / active_image.width, this.preview_height / active_image.height);
ctx.drawImage(active_image, 0, 0);
ctx.scale(1, 1);
//draw demo thumbs
for (var i in data) {
var title = data[i].title;
var function_name = 'demo';
if(typeof data[i].object[function_name] == "undefined")
continue;
data[i].object[function_name]('c_'+data[i].key, canvas);
}
}
get_effects_list() {
var list = [];
for (var i in this.Base_gui.modules) {
if (i.indexOf("effects") == -1 || i.indexOf("abstract") > -1 || i.indexOf("browser") > -1)
continue;
list.push({
title: this.get_filter_title(i),
key: i,
object: this.Base_gui.modules[i],
});
}
list.sort(function(a, b) {
var nameA = a.title.toUpperCase();
var nameB = b.title.toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
return list;
}
get_filter_title(key) {
var parts = key.split("/");
var title = parts[parts.length - 1];
//exceptions
if (title == 'negative')
title = 'invert';
title = title.replace(/_/g, ' ');
title = title.charAt(0).toUpperCase() + title.slice(1); //make first letter uppercase
return title;
}
get_function_from_path(path){
var parts = path.split("/");
var result = parts[parts.length - 1];
result = result.replace(/-/, '_');
return result;
}
}
export default Effects_browser_class;
@@ -0,0 +1,68 @@
import config from '../../../config.js';
import Effects_common_class from '../abstract/css.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';
class Effects_blur_class extends Effects_common_class {
constructor() {
super();
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
blur(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'blur');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 5, range: [0, 50]},
];
this.show_dialog('blur', params, filter_id);
}
convert_value(value, params, type) {
//adapt size to real canvas dimensions
if (type == 'preview') {
var diff = (this.POP.width_mini / this.POP.height_mini) / (config.WIDTH / config.HEIGHT);
value = value * diff;
}
return value + 'px';
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(5, null, 'preview');
ctx.filter = "blur("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'blur(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_blur_class;
@@ -0,0 +1,68 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_brightness_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
brightness(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'brightness');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 50, range: [-100, 100]},
];
this.show_dialog('brightness', params, filter_id);
}
convert_value(value) {
var system_value;
if (value > 0) {
system_value = value / 100 + 1;
}
else if (value < 0) {
system_value = value / 100 + 1;
}
else {
system_value = 1;
}
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(30, null, 'preview');
ctx.filter = "brightness("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'brightness(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_brightness_class;
@@ -0,0 +1,69 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_contrast_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
contrast(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'contrast');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 40, range: [-100, 100]},
];
this.show_dialog('contrast', params, filter_id);
}
convert_value(value) {
var system_value;
if (value > 0) {
system_value = value / 100 + 1;
}
else if (value < 0) {
system_value = value / 100 + 1;
}
else {
system_value = 1;
}
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(40, null, 'preview');
ctx.filter = "contrast("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'contrast(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_contrast_class;
@@ -0,0 +1,60 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_grayscale_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
grayscale(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'grayscale');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 100, range: [0, 100]},
];
this.show_dialog('grayscale', params, filter_id);
}
convert_value(value) {
var system_value = value / 100;
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(100, null, 'preview');
ctx.filter = "grayscale("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'grayscale(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_grayscale_class;
@@ -0,0 +1,58 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_hueRotate_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
hue_rotate(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'hue-rotate');
var params = [
{name: "value", title: "Degree:", value: filter.value ??= 90, range: [0, 360]},
];
this.show_dialog('hue-rotate', params, filter_id);
}
convert_value(value) {
return value + 'deg';
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(90, null, 'preview');
ctx.filter = "hue-rotate("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'hue-rotate(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_hueRotate_class;
@@ -0,0 +1,59 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_invert_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
invert(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'invert');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 100, range: [0, 100]},
];
this.show_dialog('invert', params, filter_id);
}
convert_value(value) {
var system_value = value / 100;
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(100, null, 'preview');
ctx.filter = "invert("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'invert(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_invert_class;
@@ -0,0 +1,69 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_saturate_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
saturate(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'saturate');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= -50, range: [-100, 100]},
];
this.show_dialog('saturate', params, filter_id);
}
convert_value(value) {
var system_value;
if (value > 0) {
system_value = value / 100 + 1;
}
else if (value < 0) {
system_value = value / 100 + 1;
}
else {
system_value = 1;
}
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(-50, null, 'preview');
ctx.filter = "saturate("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'saturate(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_saturate_class;
@@ -0,0 +1,60 @@
import Effects_common_class from '../abstract/css.js';
import Base_layers_class from './../../../core/base-layers.js';
import config from "../../../config";
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_sepia_class extends Effects_common_class {
constructor() {
super();
this.Base_layers = new Base_layers_class();
}
sepia(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'sepia');
var params = [
{name: "value", title: "Percentage:", value: filter.value ??= 60, range: [0, 100]},
];
this.show_dialog('sepia', params, filter_id);
}
convert_value(value) {
var system_value = value / 100;
return system_value;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(60, null, 'preview');
ctx.filter = "sepia("+size+")";
ctx.drawImage(canvas_thumb, 0, 0);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'sepia(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_sepia_class;
@@ -0,0 +1,79 @@
import config from '../../../config.js';
import Effects_common_class from '../abstract/css.js';
import Dialog_class from '../../../libs/popup.js';
import Effects_browser_class from '../browser.js';
import Base_layers_class from './../../../core/base-layers.js';
import alertify from './../../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_brightness_class extends Effects_common_class {
constructor() {
super();
this.POP = new Dialog_class();
this.Effects_browser = new Effects_browser_class();
this.Base_layers = new Base_layers_class();
this.preview_padding = 20;
}
shadow(filter_id) {
if (config.layer.type == null) {
alertify.error('Layer is empty.');
return;
}
var filter = this.Base_layers.find_filter_by_id(filter_id, 'shadow');
var params = [
{name: "x", title: "Offset X:", value: filter.x ??= 10, range: [-100, 100]},
{name: "y", title: "Offset Y:", value: filter.y ??= 10, range: [-100, 100]},
{name: "value", title: "Radius:", value: filter.value ??= 5, range: [0, 100]},
{name: "color", title: "Color:", value: filter.color ??= "#000000", type: 'color'},
];
this.show_dialog('shadow', params, filter_id);
}
convert_value(value, params, type) {
var system_value = value;
//adapt size to real canvas dimensions
if (type == 'preview') {
var diff = (this.POP.width_mini / this.POP.height_mini) / (config.WIDTH / config.HEIGHT);
params.x = params.x * (this.POP.width_mini / config.WIDTH);
params.y = params.y * (this.POP.height_mini / config.HEIGHT);
params.value = params.value * diff;
}
return params.x + "px " + params.y + "px " + params.value + "px " + params.color;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//draw
var size = this.convert_value(null, {x: 5, y: 5, value: 5, color: '#000000'}, 'preview');
ctx.filter = "drop-shadow("+size+")";
ctx.drawImage(canvas_thumb,
10, 10,
this.Effects_browser.preview_width - 20, this.Effects_browser.preview_height - 20);
ctx.filter = 'none';
}
render_pre(ctx, data) {
var value = this.convert_value(data.params.value, data.params, 'save');
var filter = 'drop-shadow(' + value + ')';
if(ctx.filter == 'none')
ctx.filter = filter;
else
ctx.filter += ' ' + filter;
}
render_post(ctx, data){
ctx.filter = 'none';
}
}
export default Effects_brightness_class;
@@ -0,0 +1,89 @@
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 glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_denoise_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
denoise() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Denoise',
preview: true,
effects: true,
params: [
{name: "param1", title: "Exponent:", value: 20, range: [0, 50]},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
var data = _this.change(canvas_, params);
canvas_preview.clearRect(0, 0, canvas_.width, canvas_.height);
canvas_preview.drawImage(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, params);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var param1 = parseFloat(params.param1);
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).denoise(param1).update(); //effect
return this.fx_filter;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {
param1: 20,
};
var data = this.change(canvas_thumb, params);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_denoise_class;
@@ -0,0 +1,82 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_dither_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
dither() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Dither',
preview: true,
effects: true,
params: [
{name: "param1", title: "Levels:", value: "8", range: [2, 32]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var param1 = parseFloat(params.param1);
var filtered = ImageFilters.Dither(data, param1);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
param1: 8,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_dither_class;
@@ -0,0 +1,89 @@
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 glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_dotScreen_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
dot_screen() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Dot Screen',
preview: true,
effects: true,
params: [
{name: "size", title: "Size:", value: "3", range: [1, 20]},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
var data = _this.change(canvas_, params);
canvas_preview.clearRect(0, 0, canvas_.width, canvas_.height);
canvas_preview.drawImage(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, params);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var size = parseFloat(params.size);
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).dotScreen(Math.round(canvas.width / 2), Math.round(canvas.height / 2), 0, size).update();
return this.fx_filter;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {
size: 3,
};
var data = this.change(canvas_thumb, params);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_dotScreen_class;
@@ -0,0 +1,55 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_edge_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
edge() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data) {
var filtered = ImageFilters.Edge(data);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_edge_class;
@@ -0,0 +1,55 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_emboss_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
emboss() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data) {
var filtered = ImageFilters.Emboss(data);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_emboss_class;
@@ -0,0 +1,76 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_enrich_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
enrich() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Enrich',
preview: true,
effects: true,
params: [],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var filtered = ImageFilters.Enrich(data);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_enrich_class;
@@ -0,0 +1,111 @@
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 Helper_class from './../../libs/helpers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_grains_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
}
grains() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Grains',
preview: true,
effects: true,
params: [
{name: "level", title: "Level:", value: "30", range: [0, 50]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
if (params.level == 0)
return data;
var imgData = data.data;
var H = data.height;
var W = data.width;
for (var j = 0; j < H; j++) {
for (var i = 0; i < W; i++) {
var x = (i + j * W) * 4;
if (imgData[x + 3] == 0)
continue; //transparent
//increase it's lightness
var delta = this.Helper.getRandomInt(0, params.level);
if (delta == 0)
continue;
if (imgData[x] - delta < 0)
imgData[x] = -(imgData[x] - delta);
else
imgData[x] = imgData[x] - delta;
if (imgData[x + 1] - delta < 0)
imgData[x + 1] = -(imgData[x + 1] - delta);
else
imgData[x + 1] = imgData[x + 1] - delta;
if (imgData[x + 2] - delta < 0)
imgData[x + 2] = -(imgData[x + 2] - delta);
else
imgData[x + 2] = imgData[x + 2] - delta;
}
}
return data;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
level: 30,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_grains_class;
@@ -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,111 @@
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';
class Effects_heatmap_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
heatmap() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data) {
var imgData = data.data;
var grey, RGB;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
grey = Math.round(0.2126 * imgData[i] + 0.7152 * imgData[i + 1] + 0.0722 * imgData[i + 2]);
RGB = this.color2heat(grey);
imgData[i] = RGB.R;
imgData[i + 1] = RGB.G;
imgData[i + 2] = RGB.B;
}
return data;
}
color2heat(value) {
var RGB = {R: 0, G: 0, B: 0};
value = value / 255;
if (0 <= value && value <= 1 / 8) {
RGB.R = 0;
RGB.G = 0;
RGB.B = 4 * value + .5; // .5 - 1 // b = 1/2
}
else if (1 / 8 < value && value <= 3 / 8) {
RGB.R = 0;
RGB.G = 4 * value - .5; // 0 - 1 // b = - 1/2
RGB.B = 1; // small fix
}
else if (3 / 8 < value && value <= 5 / 8) {
RGB.R = 4 * value - 1.5; // 0 - 1 // b = - 3/2
RGB.G = 1;
RGB.B = -4 * value + 2.5; // 1 - 0 // b = 5/2
}
else if (5 / 8 < value && value <= 7 / 8) {
RGB.R = 1;
RGB.G = -4 * value + 3.5; // 1 - 0 // b = 7/2
RGB.B = 0;
}
else if (7 / 8 < value && value <= 1) {
RGB.R = -4 * value + 4.5; // 1 - .5 // b = 9/2
RGB.G = 0;
RGB.B = 0;
}
else {
// should never happen - value > 1
RGB.R = .5;
RGB.G = 0;
RGB.B = 0;
}
// scale for hex conversion
RGB.R *= 255;
RGB.G *= 255;
RGB.B *= 255;
RGB.R = Math.round(RGB.R);
RGB.G = Math.round(RGB.G);
RGB.B = Math.round(RGB.B);
return RGB;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_heatmap_class;
@@ -0,0 +1,72 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_1977_class {
constructor() {
this.POP = new Dialog_class();
//this.Color_matrix = new Color_matrix_class();
this.Base_layers = new Base_layers_class();
}
1977() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "screen";
ctx2.fillStyle = 'rgba(243, 106, 188, 0.3)';
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'contrast(1.1) brightness(1.1) saturate(1.3)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_1977_class;
@@ -0,0 +1,74 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_aden_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
aden() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
var gradient = ctx2.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, "rgba(66, 10, 14, 0.2)");
gradient.addColorStop(1, "rgba(66, 10, 14, 0.2)");
ctx2.fillStyle = gradient;
ctx2.fillRect(0, 0, width, height);
//merge
ctx2.globalCompositeOperation = "darken";
ctx2.drawImage(canvas, 0, 0);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'hue-rotate(-20deg) contrast(0.9) saturate(0.85) brightness(1.2)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_aden_class;
@@ -0,0 +1,72 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_clarendon_class {
constructor() {
this.POP = new Dialog_class();
//this.Color_matrix = new Color_matrix_class();
this.Base_layers = new Base_layers_class();
}
clarendon() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.fillStyle = 'rgba(127, 187, 227, 0.2)';
ctx2.fillRect(0, 0, width, height);
//merge
ctx2.globalCompositeOperation = "overlay";
ctx2.drawImage(canvas, 0, 0);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'contrast(1.2) saturate(1.35)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_clarendon_class;
@@ -0,0 +1,71 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_gingham_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
gingham() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "soft-light";
ctx2.fillStyle = '#e6e6fa';
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'brightness(1.05) hue-rotate(-10deg)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_gingham_class;
@@ -0,0 +1,69 @@
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 'alertifyjs/build/alertify.min.js';
/*
https://github.com/una/CSSgram/blob/master/source/css/toaster.css
https://github.com/vigetlabs/canvas-instagram-filters
*/
class Effects_inkwell_class {
constructor() {
this.POP = new Dialog_class();
//this.Color_matrix = new Color_matrix_class();
this.Base_layers = new Base_layers_class();
}
inkwell() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
//apply more effects
ctx2.filter = 'sepia(0.3) contrast(1.1) brightness(1.1) grayscale(1)';
ctx2.drawImage(canvas, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_inkwell_class;
@@ -0,0 +1,75 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_lofi_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
lofi() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "multiply";
var min = Math.min(width, height);
var gradient = ctx2.createRadialGradient(width / 2, height / 2, min * 0.7, width / 2, height / 2, min * 1.5);
gradient.addColorStop(0, "rgba(0,0,0,0)");
gradient.addColorStop(1, "#222222");
ctx2.fillStyle = gradient;
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'saturate(1.1) contrast(1.5)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_lofi_class;
@@ -0,0 +1,79 @@
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 'alertifyjs/build/alertify.min.js';
/*
https://github.com/una/CSSgram/blob/master/source/css/toaster.css
https://github.com/vigetlabs/canvas-instagram-filters
*/
class Effects_toaster_class {
constructor() {
this.POP = new Dialog_class();
//this.Color_matrix = new Color_matrix_class();
this.Base_layers = new Base_layers_class();
}
toaster() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "screen";
var gradient = ctx2.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width * 0.6);
gradient.addColorStop(0, "#804e0f");
gradient.addColorStop(1, "#3b003b");
ctx2.fillStyle = gradient;
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'contrast(1.5) brightness(0.9)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_toaster_class;
@@ -0,0 +1,71 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_valencia_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
valencia() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "exclusion";
ctx2.fillStyle = '3a0339';
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'contrast(1.08) brightness(1.08) sepia(0.08)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_valencia_class;
@@ -0,0 +1,75 @@
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 'alertifyjs/build/alertify.min.js';
class Effects_xpro2_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
xpro2() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
//create temp canvas
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
//merge
ctx2.globalCompositeOperation = "color-burn";
var min = Math.min(width, height);
var gradient = ctx2.createRadialGradient(width / 2, height / 2, min * 0.4, width / 2, height / 2, min * 1.1);
gradient.addColorStop(0, "#e6e7e0");
gradient.addColorStop(1, "rgba(43, 42, 161, 0.6)");
ctx2.fillStyle = gradient;
ctx2.fillRect(0, 0, width, height);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'sepia(0.3)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_xpro2_class;
@@ -0,0 +1,86 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_mosaic_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
mosaic() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Mosaic',
preview: true,
effects: true,
params: [
{name: "size", title: "Size:", value: 10, range: [1, 100]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var size = parseFloat(params.size);
//convert % to px
size = Math.min(data.width, data.height) * size / 100;
size = Math.round(size);
var filtered = ImageFilters.Mosaic(data, size);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
size: 10,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_mosaic_class;
@@ -0,0 +1,82 @@
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 glfx from './../../libs/glfx.js';
import ImageFilters_class from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_nightVision_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
this.ImageFilters = ImageFilters_class;
}
night_vision() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
//create second copy
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, 0, 0);
// green overlay
var img = ctx2.getImageData(0, 0, width, height);
//RGB corrections
var img = this.ImageFilters.ColorTransformFilter(img, 1, 1, 1, 1, 0, 100, 0, 1);
//hue/saturation/luminance
var img = this.ImageFilters.HSLAdjustment(img, 0, 0, -50);
ctx2.putImageData(img, 0, 0);
//vignete
var texture = this.fx_filter.texture(canvas2);
this.fx_filter.draw(texture).vignette(0.2, 0.9).update(); //effect
canvas2 = this.fx_filter;
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {};
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_nightVision_class;
@@ -0,0 +1,85 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_oil_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
oil() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Oil',
preview: true,
effects: true,
params: [
{name: "param1", title: "Range:", value: 2, range: [1, 10]},
{name: "param2", title: "Levels:", value: "32", range: [1, 256]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var param1 = parseFloat(params.param1);
var param2 = parseInt(params.param2);
var filtered = ImageFilters.Oil(data, param1, param2);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
param1: 2,
param2: 32,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_oil_class;
@@ -0,0 +1,73 @@
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';
class Effects_pencil_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
pencil() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, width, height) {
var offset = Math.min(width, height) / 1000;
offset = Math.ceil(offset);
//create second copy
var canvas2 = document.createElement('canvas');
var ctx2 = canvas2.getContext("2d");
canvas2.width = width;
canvas2.height = height;
ctx2.drawImage(canvas, -offset, -offset);
//merge
ctx2.globalCompositeOperation = "difference";
ctx2.drawImage(canvas, 0, 0);
ctx2.globalCompositeOperation = "source-over";
//apply more effects
ctx2.filter = 'brightness(2) invert(1) grayscale(1)';
ctx2.drawImage(canvas2, 0, 0);
ctx2.filter = 'none';
return canvas2;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {};
var data = this.change(canvas_thumb, canvas_thumb.width, canvas_thumb.height);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_pencil_class;
@@ -0,0 +1,82 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_sharpen_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
sharpen() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Sharpen',
preview: true,
effects: true,
params: [
{name: "param1", title: "Factor:", value: "3", range: [1, 10], step: 0.1},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, params) {
var param1 = parseFloat(params.param1);
var filtered = ImageFilters.Sharpen(data, param1);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var params = {
param1: 3,
}
var data = this.change(img, params);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_sharpen_class;
@@ -0,0 +1,55 @@
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 ImageFilters from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_solarize_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
solarize() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data) {
var filtered = ImageFilters.Solarize(data);
return filtered;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var img = ctx.getImageData(0, 0, canvas_thumb.width, canvas_thumb.height);
var data = this.change(img);
ctx.putImageData(data, 0, 0);
}
}
export default Effects_solarize_class;
@@ -0,0 +1,144 @@
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 ImageFilters from './../../libs/imagefilters.js';
import glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_tiltShift_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
tilt_shift() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Tilt Shift',
preview: true,
effects: true,
params: [
//extra
{name: "param7", title: "Saturation:", value: "3", range: [0, 20]},
{name: "param8", title: "Sharpen:", value: "1", range: [0, 5]},
//main
{name: "param1", title: "Blur Radius:", value: 10, range: [0, 30]},
{name: "param2", title: "Gradient Radius:", value: 70, range: [40, 100]},
//startX, startY, endX, endY
{name: "param3", title: "X start:", value: 0, range: [0, 100]},
{name: "param4", title: "Y start:", value: 50, range: [0, 100]},
{name: "param5", title: "X end:", value: 100, range: [0, 100]},
{name: "param6", title: "Y end:", value: 50, range: [0, 100]},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
//recalc param by size
_this.change(canvas_, params);
//convert % to px for line
params.param3 = canvas_.width * params.param3 / 100;
params.param4 = canvas_.height * params.param4 / 100;
params.param5 = canvas_.width * params.param5 / 100;
params.param6 = canvas_.height * params.param6 / 100;
//draw line
canvas_preview.beginPath();
canvas_preview.strokeStyle = "#ff0000";
canvas_preview.lineWidth = 1;
canvas_preview.moveTo(params.param3 + 0.5, params.param4 + 0.5);
canvas_preview.lineTo(params.param5 + 0.5, params.param6 + 0.5);
canvas_preview.stroke();
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
this.change(canvas, params);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var param1 = parseInt(params.param1);
var param2 = parseInt(params.param2);
var param3 = parseInt(params.param3);
var param4 = parseInt(params.param4);
var param5 = parseInt(params.param5);
var param6 = parseInt(params.param6);
var param7 = parseInt(params.param7);
var param8 = parseInt(params.param8);
//convert % to px
param1 = canvas.height * param1 / 100;
param2 = canvas.height * param2 / 100;
param3 = canvas.width * param3 / 100;
param4 = canvas.height * param4 / 100;
param5 = canvas.width * param5 / 100;
param6 = canvas.height * param6 / 100;
var ctx = canvas.getContext("2d");
//main effect
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).tiltShift(param3, param4, param5, param6, param1, param2).update();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.fx_filter, 0, 0);
//saturation
var data = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = ImageFilters.HSLAdjustment(data, 0, param7, 0);
ctx.putImageData(data, 0, 0);
//sharpen
var data = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = ImageFilters.Sharpen(data, param8);
ctx.putImageData(data, 0, 0);
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var params = {
param7: 3,
param8: 1,
param1: 10,
param2: 70,
param3: 0,
param4: 50,
param5: 100,
param6: 50,
}
var data = this.change(canvas, params);
}
}
export default Effects_tiltShift_class;
@@ -0,0 +1,89 @@
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 glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_vibrance_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
vibrance() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Vibrance',
preview: true,
effects: true,
params: [
{name: "level", title: "Level:", value: "0.5", range: [-1, 1], step: 0.01},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
var data = _this.change(canvas_, params);
canvas_preview.clearRect(0, 0, canvas_.width, canvas_.height);
canvas_preview.drawImage(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, params);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var param1 = parseFloat(params.level);
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).vibrance(param1).update(); //effect
return this.fx_filter;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {
level: 0.5,
};
var data = this.change(canvas_thumb, params);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_vibrance_class;
@@ -0,0 +1,92 @@
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 glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_vignette_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
vignette() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Vignette',
preview: true,
effects: true,
params: [
{name: "param1", title: "Level:", value: "0.5", range: [0, 1], step: 0.01},
{name: "param2", title: "Size:", value: "0.5", range: [0, 1], step: 0.01},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
var data = _this.change(canvas_, params);
canvas_preview.clearRect(0, 0, canvas_.width, canvas_.height);
canvas_preview.drawImage(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, params);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var param1 = parseFloat(params.param1);
var param2 = parseFloat(params.param2);
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).vignette(param1, param2).update(); //effect
return this.fx_filter;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {
param1: 0.5,
param2: 0.5,
};
var data = this.change(canvas_thumb, params);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_vignette_class;
@@ -0,0 +1,77 @@
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 Vintage_class from './../../libs/vintage.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_vintage_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Vintage = new Vintage_class(config.WIDTH, config.HEIGHT);
}
vintage() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
this.Vintage.reset_random_values(config.WIDTH, config.HEIGHT);
var settings = {
title: 'Vintage',
preview: true,
effects: true,
params: [
{name: "level", title: "Level:", value: 50, range: [0, 100]},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
_this.change(canvas_, params);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
this.change(canvas, params);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
var level = parseInt(params.level);
this.Vintage.apply_all(canvas, level);
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
ctx.drawImage(canvas_thumb, 0, 0);
//now update
var params = {
level: 50,
};
this.change(canvas, params);
}
}
export default Effects_vintage_class;
@@ -0,0 +1,102 @@
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 glfx from './../../libs/glfx.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Effects_zoomBlur_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.fx_filter = false;
}
zoom_blur() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get layer size
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var settings = {
title: 'Zoom blur',
preview: true,
effects: true,
params: [
{name: "param1", title: "Strength:", value: "0.3", range: [0, 1], step: 0.01},
{name: "param2", title: "Center x:", value: Math.round(canvas.width / 2), range: [0, canvas.width]},
{name: "param3", title: "Center y:", value: Math.round(canvas.height / 2), range: [0, canvas.height]},
],
on_change: function (params, canvas_preview, w, h, canvas_) {
//recalc param by size
params.param2 = params.param2 / canvas.width * w;
params.param3 = params.param3 / canvas.height * h;
var data = _this.change(canvas_, params);
canvas_preview.clearRect(0, 0, canvas_.width, canvas_.height);
canvas_preview.drawImage(data, 0, 0);
},
on_finish: function (params) {
_this.save(params);
},
};
this.POP.show(settings);
}
save(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var data = this.change(canvas, params);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(canvas, params) {
if (this.fx_filter == false) {
//init glfx lib
this.fx_filter = glfx.canvas();
}
var param1 = parseFloat(params.param1);
var param2 = parseInt(params.param2);
var param3 = parseInt(params.param3);
var texture = this.fx_filter.texture(canvas);
this.fx_filter.draw(texture).zoomBlur(param2, param3, param1).update(); //effect
return this.fx_filter;
}
demo(canvas_id, canvas_thumb){
var canvas = document.getElementById(canvas_id);
var ctx = canvas.getContext("2d");
//modify
var params = {
param1: 0.3,
param2: Math.round(canvas_thumb.width / 2),
param3: Math.round(canvas_thumb.height / 2),
};
var data = this.change(canvas_thumb, params);
//draw
ctx.drawImage(data, 0, 0);
}
}
export default Effects_zoomBlur_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: 'wide',
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,137 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_gui_class from './../../core/base-gui.js';
import Base_layers_class from './../../core/base-layers.js';
import Helper_class from './../../libs/helpers.js';
import Dialog_class from './../../libs/popup.js';
import Tools_settings_class from './../tools/settings.js';
/**
* manages files / new
*
* @author ViliusL
*/
class File_new_class {
constructor() {
this.Base_gui = new Base_gui_class();
this.Base_layers = new Base_layers_class();
this.POP = new Dialog_class();
this.Helper = new Helper_class();
this.Tools_settings = new Tools_settings_class();
}
new () {
var _this = this;
var width = config.WIDTH;
var height = config.HEIGHT;
var common_dimensions = this.Base_gui.common_dimensions;
var resolution_types = ['Custom'];
var units = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
for (var i in common_dimensions) {
var value = common_dimensions[i];
resolution_types.push(value[0] + 'x' + value[1] + ' - ' + value[2]);
}
var transparency_cookie = this.Helper.getCookie('transparency');
if (transparency_cookie === null) {
//default
transparency_cookie = false;
}
if (transparency_cookie) {
var transparency = true;
}
else {
var transparency = false;
}
//convert units
width = this.Helper.get_user_unit(width, units, resolution);
height = this.Helper.get_user_unit(height, units, resolution);
var settings = {
title: 'New file',
params: [
{name: "width", title: "Width:", value: width, comment: units},
{name: "height", title: "Height:", value: height, comment: units},
{name: "resolution_type", title: "Resolution:", values: resolution_types},
{name: "layout", title: "Layout:", value: "Custom", values: ["Custom", "Landscape", "Portrait"]},
{name: "transparency", title: "Transparent:", value: transparency},
],
on_finish: function (params) {
_this.new_handler(params);
},
};
this.POP.show(settings);
}
async new_handler(response) {
var width = parseFloat(response.width);
var height = parseFloat(response.height);
var resolution_type = response.resolution_type;
var transparency = response.transparency;
var units = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
if (resolution_type != 'Custom') {
var dim = resolution_type.split(" ");
dim = dim[0].split("x");
width = parseInt(dim[0]);
height = parseInt(dim[1]);
if(response.layout == 'Portrait'){
var tmp = width;
width = height;
height = tmp;
}
}
else {
//convert units
width = this.Helper.get_internal_unit(width, units, resolution);
height = this.Helper.get_internal_unit(height, units, resolution);
}
// Prepare layers
app.State.do_action(
new app.Actions.Bundle_action('new_file', 'New File', [
new app.Actions.Refresh_action_attributes_action('undo'),
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
TRANSPARENCY: !!transparency,
WIDTH: parseInt(width),
HEIGHT: parseInt(height),
ALPHA: 255,
COLOR: '#008000',
mouse: {},
visible_width: null,
visible_height: null,
user_fonts: {}
}),
new app.Actions.Prepare_canvas_action('do'),
new app.Actions.Refresh_action_attributes_action('do'),
new app.Actions.Reset_layers_action(),
new app.Actions.Init_canvas_zoom_action(),
new app.Actions.Insert_layer_action({})
])
);
//sleep, lets wait till DOM is finished
await new Promise(r => setTimeout(r, 10));
//fit to screen?
this.Base_gui.GUI_preview.zoom_auto(true);
// Save transparency
if (transparency) {
this.Helper.setCookie('transparency', 1);
}
else {
this.Helper.setCookie('transparency', 0);
}
}
}
export default File_new_class;
@@ -0,0 +1,708 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import Base_gui_class from './../../core/base-gui.js';
import Dialog_class from './../../libs/popup.js';
import Helper_class from './../../libs/helpers.js';
import Clipboard_class from './../../libs/clipboard.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import EXIF from './../../../../node_modules/exif-js/exif.js';
import GUI_tools_class from "../../core/gui/gui-tools";
import semver_compare from './../../../../node_modules/semver-compare/';
var instance = null;
/**
* manages files / open
*
* @author ViliusL
*/
class File_open_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
var _this = this;
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Base_gui = new Base_gui_class();
this.Helper = new Helper_class();
this.GUI_tools = new GUI_tools_class();
//clipboard class
this.Clipboard_class = new Clipboard_class(function (data, w, h) {
_this.on_paste(data, w, h);
});
this.events();
this.maybe_file_open_url_handler();
}
events() {
var _this = this;
window.ondrop = function (e) {
//drop
e.preventDefault();
_this.open_handler(e);
};
window.ondragover = function (e) {
e.preventDefault();
};
document.addEventListener('keydown', (event) => {
var code = event.key.toLowerCase();
if (this.Helper.is_input(event.target))
return;
if (code == "o") {
//open
this.open_file();
event.preventDefault();
}
}, false);
}
on_paste(data, width, height) {
var new_layer = {
name: 'Paste',
type: 'image',
data: data,
};
app.State.do_action(
new app.Actions.Insert_layer_action(new_layer)
);
}
open_file() {
var _this = this;
alertify.success('You can also drag and drop items into browser.');
document.getElementById("tmp").innerHTML = '';
var a = document.createElement('input');
a.setAttribute("id", "file_open");
a.type = 'file';
a.multiple = 'multiple';
document.getElementById("tmp").appendChild(a);
document.getElementById('file_open').addEventListener('change', function (e) {
_this.open_handler(e);
}, false);
//force click
document.querySelector('#file_open').click();
}
open_webcam(){
var _this = this;
var video = document.createElement('video');
video.autoplay = true;
video.style.maxWidth = '100%';
var track = null;
function handleSuccess(stream) {
track = stream.getTracks()[0];
video.srcObject = stream;
}
function handleError(error) {
alertify.error('Sorry, cold not load getUserMedia() data: ' + error);
}
var settings = {
title: 'Webcam',
params: [
{title: "Stream:", html: '<div id="webcam_container"></div>'},
],
on_load: function(params){
document.getElementById('webcam_container').appendChild(video);
},
on_finish: function(params){
//capture data
var width = video.videoWidth;
var height = video.videoHeight;
var tmpCanvas = document.createElement('canvas');
var tmpCanvasCtx = tmpCanvas.getContext("2d");
tmpCanvas.width = width;
tmpCanvas.height = height;
tmpCanvasCtx.drawImage(video, 0, 0);
//create requested layer
var new_layer = {
name: "Webcam #" + _this.Base_layers.auto_increment,
type: 'image',
data: tmpCanvas.toDataURL("image/png"),
width: width,
height: height,
width_original: width,
height_original: height,
};
app.State.do_action(
new app.Actions.Bundle_action('open_file_webcam', 'Open File Webcam', [
new app.Actions.Insert_layer_action(new_layer),
new app.Actions.Autoresize_canvas_action(width, height, null, true, true)
])
);
//destroy
if(track != null){
track.stop();
}
video.pause();
video.src = "";
video.load();
},
on_cancel: function(params){
if(track != null){
track.stop();
}
video.pause();
video.src = "";
video.load();
},
};
this.POP.show(settings);
navigator.mediaDevices.getUserMedia({audio: false, video: true})
.then(handleSuccess)
.catch(handleError);
}
open_dir() {
var _this = this;
document.getElementById("tmp").innerHTML = '';
var a = document.createElement('input');
a.setAttribute("id", "file_open_dir");
a.type = 'file';
a.webkitdirectory = 'webkitdirectory';
document.getElementById("tmp").appendChild(a);
document.getElementById('file_open_dir').addEventListener('change', function (e) {
_this.open_handler(e);
}, false);
//force click
document.querySelector('#file_open_dir').click();
}
/**
* opens data URLs, like: "data:image/png;base64,xxxxxx"
*
* data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAG0lEQVQYV2N89+7df0FBQQbG/////3///j0DAF9wCsg9spQfAAAAAElFTkSuQmCC
*/
open_data_url() {
var _this = this;
var settings = {
title: 'Open data URL',
params: [
{name: "data", title: "Data URL:", type: "textarea", value: ""},
],
on_finish: function (params) {
_this.file_open_data_url_handler(params.data);
},
};
this.POP.show(settings);
}
file_open_data_url_handler(data) {
var _this = this;
if (data == '')
return;
var img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
var new_layer = {
name: "Data URL",
type: 'image',
link: img,
width: img.width,
height: img.height,
width_original: img.width,
height_original: img.height,
};
app.State.do_action(
new app.Actions.Bundle_action('open_file_data_url', 'Open File Data URL', [
new app.Actions.Insert_layer_action(new_layer),
new app.Actions.Autoresize_canvas_action(img.width, img.height, null, true, true)
])
);
img.onload = function () {
config.need_render = true;
};
};
img.onerror = function (ex) {
alertify.error('Sorry, image could not be loaded. Try copy image and paste it.');
};
img.src = data;
}
open_url() {
var _this = this;
var settings = {
title: 'Open URL',
params: [
{name: "url", title: "URL:", value: ""},
],
on_finish: function (params) {
_this.file_open_url_handler(params);
},
};
this.POP.show(settings);
}
async open_handler(e) {
var _this = this;
var files = e.target.files;
var auto_increment = this.Base_layers.auto_increment;
if (files == undefined) {
//drag and drop
files = e.dataTransfer.files;
}
//sort
var orders = [];
for (var i = 0, f; i < files.length; i++) {
orders.push(files[i].name);
}
orders.sort();
var order_map = [];
for (var i in orders) {
order_map[orders[i]] = parseInt(i);
}
//check if dropped directory
var dir_opened = false;
if (e.dataTransfer && e.dataTransfer.items) {
var items = e.dataTransfer.items;
for (var i=0; i<items.length; i++) {
var item = items[i].webkitGetAsEntry();
if(item && item.isDirectory){
dir_opened = true;
}
}
}
for (var i = 0, f; i < files.length; i++) {
f = files[i];
if (!f.type.match('image.*') && !f.name.match('.json')) {
if(dir_opened == false) {
alertify.error('Wrong file type, must be image or json.');
}
continue;
}
if (files.length == 1) {
this.SAVE_NAME = f.name.split('.')[f.name.split('.').length - 2];
}
var FR = new FileReader();
FR.file = files[i];
FR.onload = function (event) {
if (this.file.type.match('image.*')) {
var order = auto_increment + order_map[this.file.name];
//image
var new_layer = {
name: this.file.name,
type: 'image',
data: event.target.result,
order: order,
_exif: _this.extract_exif(this.file)
};
app.State.do_action(
new app.Actions.Bundle_action('open_image', 'Open Image', [
new app.Actions.Insert_layer_action(new_layer)
])
);
}
else {
//json
var response = _this.load_json(event.target.result);
if (response === true) {
return false;
}
}
};
if (f.type == "text/plain")
FR.readAsText(f);
else if (f.name.match('.json'))
FR.readAsText(f);
else
FR.readAsDataURL(f);
//sleep after last image import, it maybe not be finished yet
await new Promise(r => setTimeout(r, 10));
}
//try to open dropped directory
if (e.dataTransfer && e.dataTransfer.items) {
var items = e.dataTransfer.items;
for (var i=0; i<items.length; i++) {
var item = items[i].webkitGetAsEntry();
if (item && item.isDirectory == true) {
this.traverseFileTree(item);
}
}
}
}
traverseFileTree(item, path) {
var _this = this;
var auto_increment = this.Base_layers.auto_increment;
path = path || "";
if (item.isFile) {
item.file(async function(file) {
var FR = new FileReader();
FR.file = file;
FR.onload = function (event) {
if (this.file.type.match('image.*')
//below is fix for firefox, it has empty type
|| (this.file.type == '' && this.file.name.match(/\.(png|jpg|jpeg|webp|gif|avif)/g))) {
//image
var new_layer = {
name: this.file.name,
type: 'image',
data: event.target.result,
_exif: _this.extract_exif(this.file)
};
app.State.do_action(
new app.Actions.Bundle_action('open_image', 'Open Image', [
new app.Actions.Insert_layer_action(new_layer)
])
);
}
};
FR.readAsDataURL(file);
//sleep after last image import, it maybe not be finished yet
await new Promise(r => setTimeout(r, 10));
});
}
else if (item.isDirectory) {
// Get folder contents
var dirReader = item.createReader();
dirReader.readEntries(function(entries) {
for (var i=0; i<entries.length; i++) {
_this.traverseFileTree(entries[i], path + item.name + "/");
}
});
}
}
open_template_test(){
var _this = this;
this.Base_layers.debug_rendering = true;
window.fetch("images/test-collection.json").then(function(response) {
return response.json();
}).then(function(json) {
_this.load_json(json, false);
}).catch(function(ex) {
alertify.error('Sorry, image could not be loaded.');
});
}
/**
* check if url has url params, for example: https://viliusle.github.io/miniPaint/?image=http://i.imgur.com/ATda8Ae.jpg
*/
maybe_file_open_url_handler() {
var _this = this;
var url_params = this.Helper.get_url_parameters();
if (url_params.image != undefined) {
this.open_resource(url_params.image);
}
}
/**
* includes provided resource (image or json)
*
* @param string resource_url
*/
open_resource(resource_url) {
var _this = this;
if(resource_url.toLowerCase().indexOf('.json') == resource_url.length - 5){
//load json
window.fetch(resource_url).then(function(response) {
return response.json();
}).then(function(json) {
_this.load_json(json, false);
}).catch(function(ex) {
alertify.error('Sorry, image could not be loaded.');
});
}
else{
//load image
var data = {
url: resource_url,
};
this.file_open_url_handler(data);
}
}
//handler for open url. Example url: http://i.imgur.com/ATda8Ae.jpg
file_open_url_handler(user_response) {
var _this = this;
var url = user_response.url;
if (url == '')
return;
var layer_name = url.replace(/^.*[\\\/]/, '');
var img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
var new_layer = {
name: layer_name,
type: 'image',
link: img,
width: img.width,
height: img.height,
width_original: img.width,
height_original: img.height,
};
img.onload = function () {
config.need_render = true;
};
app.State.do_action(
new app.Actions.Bundle_action('open_file_url', 'Open File URL', [
new app.Actions.Insert_layer_action(new_layer),
new app.Actions.Autoresize_canvas_action(img.width, img.height, null, true, true)
])
);
};
img.onerror = function (ex) {
alertify.error('Sorry, image could not be loaded. Try copy image and paste it.');
};
img.src = url;
}
async load_json(data) {
var json;
if(typeof data == 'string')
json = JSON.parse(data);
else
json = data;
if (json.info.version == undefined) {
json.info.version = "3.0.0";
}
//migration
if(semver_compare(json.info.version, '4.0.0') < 0) {
//convert from v3 to v4
for (var i in json.layers) {
//layers data
json.layers[i].id = (parseInt(i) + 1);
json.layers[i].opacity = json.layers[i].opacity * 100 || 100;
json.layers[i].type = "image";
json.layers[i].width = json.info.width;
json.layers[i].height = json.info.height;
json.layers[i].visible = (json.layers[i].visible == true); //convert to boolean
delete json.layers[i].title;
}
json.data = [];
for (var i in json.image_data) {
//image data
var new_id = null;
for (var j in json.layers) {
if (json.layers[j].name == json.image_data[i].name) {
new_id = json.layers[j].id;
}
}
if (new_id == null)
continue;
json.data.push(
{
id: new_id,
data: json.image_data[i].data,
}
);
}
}
if(semver_compare(json.info.version, '4.5.0') < 0) {
//migrate "rectangle", "circle" and "line" types to "shape"
for (var i in json.layers) {
var old_type = json.layers[i].type;
if(old_type == 'line' && json.layers[i].params.type.value == "Arrow"){
//migrate line (type=arrow) to arrow.
json.layers[i].type = 'arrow';
delete json.layers[i].params.type;
json.layers[i].render_function = ["arrow", "render"];
}
if(old_type == 'rectangle' || old_type == 'circle'){
//migrate params
json.layers[i].params.border_size = json.layers[i].params.size;
delete json.layers[i].params.size;
if(json.layers[i].params.fill == true) {
json.layers[i].params.border = false;
}
else{
json.layers[i].params.border = true;
}
json.layers[i].params.border_color = json.layers[i].color;
json.layers[i].params.fill_color = json.layers[i].color;
json.layers[i].color = null;
}
if(old_type == 'circle'){
//rename circle to ellipse
json.layers[i].type = 'ellipse';
json.layers[i].render_function = ["ellipse", "render"];
}
}
}
if(semver_compare(json.info.version, '4.8.0') < 0) {
//migrate "borders" layer to rectangle
for (var i in json.layers) {
var old_type = json.layers[i].type;
if(old_type == 'borders'){
json.layers[i].type = 'rectangle';
json.layers[i].name += ' (legacy)';
json.layers[i].params = {
radius: 0,
fill: false,
square: false,
border_size: json.layers[i].params.size,
border: true,
border_color: json.layers[i].color,
fill_color: "#000000",
};
json.layers[i].render_function = ["rectangle", "render"];
}
}
}
if(semver_compare(json.info.version, '4.11.0') < 0) {
//migrate star and star24 objects
for (var i in json.layers) {
var old_type = json.layers[i].type;
if(old_type == 'star' && typeof json.layers[i].params.corners == "undefined"){
json.layers[i].params.corners = 5;
json.layers[i].params.inner_radius = 40;
json.layers[i].render_function = ["star", "render"];
}
else if(old_type == 'star24'){
json.layers[i].type = 'star';
json.layers[i].params.corners = 24;
json.layers[i].params.inner_radius = 80;
json.layers[i].render_function = ["star", "render"];
}
}
}
const actions = [];
//reset zoom
await this.Base_gui.GUI_preview.zoom(100); //reset zoom
//set attributes
actions.push(
new app.Actions.Refresh_action_attributes_action('undo'),
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
ZOOM: 1,
WIDTH: parseInt(json.info.width),
HEIGHT: parseInt(json.info.height),
user_fonts: json.user_fonts || {}
}),
new app.Actions.Reset_layers_action(),
new app.Actions.Prepare_canvas_action('do'),
new app.Actions.Refresh_action_attributes_action('do')
);
var max_id_order = 0;
for (var i in json.layers) {
var value = json.layers[i];
if(value.id > max_id_order)
max_id_order = value.id;
if(typeof value.order != undefined && value.order > max_id_order)
max_id_order = value.order;
if (value.type == 'image') {
//add image data
value.link = null;
for (var j in json.data) {
if (json.data[j].id == value.id) {
value.data = json.data[j].data;
}
}
}
actions.push(
new app.Actions.Insert_layer_action(value, false)
);
}
if (json.info.layer_active != undefined) {
actions.push(
new app.Actions.Select_layer_action(json.info.layer_active, true)
);
}
if (json.info.guides != undefined) {
config.guides = json.info.guides;
}
actions.push(
new app.Actions.Set_object_property_action(this.Base_layers, 'auto_increment', max_id_order + 1),
new app.Actions.Update_config_action({
WIDTH: parseInt(json.info.width),
HEIGHT: parseInt(json.info.height),
}),
new app.Actions.Prepare_canvas_action('do')
);
await app.State.do_action(
new app.Actions.Bundle_action('open_json_file', 'Open JSON File', actions)
);
}
/**
* Returns an action that saves the exif data of the provided object to the current layer
*/
extract_exif(object) {
var exif_data = {
general: [],
exif: [],
};
//exif data
EXIF.getData(object, function () {
exif_data.exif = this.exifdata;
delete this.exifdata.thumbnail;
});
//general
if (object.name != undefined)
exif_data.general.Name = object.name;
if (object.size != undefined)
exif_data.general.Size = this.Helper.number_format(object.size / 1000, 2) + ' KB';
if (object.type != undefined)
exif_data.general.Type = object.type;
if (object.lastModified != undefined)
exif_data.general['Last modified'] = this.Helper.format_time(object.lastModified);
return exif_data;
}
search(){
this.GUI_tools.activate_tool('media');
}
}
export default File_open_class;
@@ -0,0 +1,14 @@
/**
* manages files / print
*
* @author ViliusL
*/
class File_print_class {
print() {
window.print();
}
}
export default File_print_class;
@@ -0,0 +1,46 @@
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import File_open_class from './open.js';
/**
* manages files / quick-load
*
* @author ViliusL
*/
class File_quickload_class {
constructor() {
this.Base_layers = new Base_layers_class();
this.File_open = new File_open_class();
this.set_events();
}
set_events() {
var _this = this;
document.addEventListener('keydown', function (event) {
var code = event.keyCode;
if (code == 121) {
//F10
_this.quickload();
event.preventDefault();
}
}, false);
}
quickload() {
//load image data
var json = localStorage.getItem('quicksave_data');
if (json == '' || json == null) {
//nothing was found
return false;
}
this.File_open.load_json(json);
}
}
export default File_quickload_class;
@@ -0,0 +1,45 @@
import config from './../../config.js';
import File_save_class from './save.js';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
/**
* manages files / quick-save
*
* @author ViliusL
*/
class File_quicksave_class {
constructor() {
this.POP = new Dialog_class();
this.File_save = new File_save_class();
this.set_events();
}
set_events() {
var _this = this;
document.addEventListener('keydown', function (event) {
var code = event.keyCode;
if (code == 120) {
//F9
_this.quicksave();
}
}, false);
}
quicksave() {
//save image data
var data_json = this.File_save.export_as_json();
if (data_json.length > 5000000) {
alertify.error('Sorry, image is too big, max 5 MB.');
return false;
}
localStorage.setItem('quicksave_data', data_json);
}
}
export default File_quicksave_class;
@@ -0,0 +1,827 @@
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';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import canvasToBlob from './../../../../node_modules/blueimp-canvas-to-blob/js/canvas-to-blob.min.js';
import filesaver from './../../../../node_modules/file-saver/dist/FileSaver.min.js';
import GIF from './../../../../node_modules/gif.js.optimized/';
import CanvasToTIFF from './../../libs/canvastotiff.js';
import TiffWriter from './../../libs/tiff-writer.js';
import PdfWriter from './../../libs/pdf-writer.js';
import Tools_settings_class from "../tools/settings";
var instance = null;
/**
* manages files / save
*
* @author ViliusL
*/
class File_save_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.POP = new Dialog_class();
this.Tools_settings = new Tools_settings_class();
this.set_events();
//save types config
this.SAVE_TYPES = {
PNG: "Portable Network Graphics",
JPG: "JPG/JPEG Format",
//AVIF: "AV1 Image File Format", //just uncomment it in future to make it work
JSON: "Full layers data",
WEBP: "Weppy File Format",
GIF: "Graphics Interchange Format",
BMP: "Windows Bitmap",
TIFF: "TIFF (RGBA)",
TIFF_CMYK: "TIFF (CMYK, print)",
TIFF_LAYERS: "TIFF (Multilayer)",
PDF: "PDF Document (RGB)",
PDF_CMYK: "PDF Document (CMYK, print)",
};
this.default_extension = 'PNG';
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.key.toLowerCase();
if (this.Helper.is_input(event.target))
return;
if (code == "s") {
if(event.shiftKey){
//export
this.save();
}
else{
//save
this.export();
}
event.preventDefault();
}
}, false);
}
/**
* saves as non destructive mode (including layers, RAW)
*/
save(){
var types = JSON.parse(JSON.stringify(this.SAVE_TYPES));
for(var i in types){
if(i != 'JSON'){
delete types[i];
}
}
this.save_general(types, 'Save as');
}
/**
* save as encoded image
*/
export(){
var types = JSON.parse(JSON.stringify(this.SAVE_TYPES));
delete types.JSON;
this.save_general(types, 'Export');
}
save_general(file_types, title) {
var _this = this;
//find default format
var save_default = null;
var save_default_cookie = this.Helper.getCookie('save_default');
for(var i in file_types) {
if(save_default_cookie == i){
save_default = i;
break;
}
}
if(save_default == null){
save_default = Object.keys(file_types)[0];
}
save_default = save_default + " - " + file_types[save_default];
var calc_size_value = false;
var calc_size = false;
if (config.WIDTH * config.HEIGHT < 1000000) {
calc_size_value = true;
calc_size = true;
}
var file_name = config.layers[0].name;
var parts = file_name.split('.');
if (parts.length > 1)
file_name = parts[parts.length - 2];
file_name = file_name.replace(/ /g, "-");
file_name = this.Helper.escapeHtml(file_name);
var save_types = [];
for(var i in file_types) {
save_types.push(i + " - " + file_types[i]);
}
var save_layers_types = [
'All',
'Selected',
'Separated',
'Separated (original types)',
];
var resolution = this.Tools_settings.get_setting('resolution');
var settings = {
title: title,
params: [
{name: "name", title: "File name:", value: file_name},
{name: "type", title: "Save as type:", values: save_types, value: save_default},
{name: "quality", title: "Quality:", value: 90, range: [1, 100]},
{title: "File size:", html: '<span id="file_size">-</span>'},
{title: "Resolution:", value: resolution},
{name: "calc_size", title: "Show file size:", value: calc_size_value},
{name: "layers", title: "Save layers:", values: save_layers_types},
{name: "delay", title: "Gif delay:", value: 400},
],
on_change: function (params, canvas_preview, w, h) {
_this.save_dialog_onchange(true);
},
on_finish: function (params) {
// These types handle their own layering internally — skip the separated loop.
var _type = params.type ? params.type.split(' ')[0] : '';
var _multilayerType = _type === 'TIFF_LAYERS' || _type === 'TIFF_CMYK'
|| _type === 'PDF' || _type === 'PDF_CMYK';
if (!_multilayerType && (params.layers == 'Separated' || params.layers == 'Separated (original types)')) {
var active_layer = config.layer.id;
var original_layer_type = params.layers;
//alter params
params.layers = 'Selected';
for (var i in config.layers) {
if (config.layers[i].visible == false)
continue;
//detect type
if (original_layer_type == 'Separated (original types)') {
//detect type from file name
params.type = _this.SAVE_TYPES[_this.default_extension];
for (var j in _this.SAVE_TYPES) {
if (_this.Helper.strpos(config.layers[i].name.toLowerCase(), '.' + j.toLowerCase()) !== false) {
params.type = j;
break;
}
}
}
new app.Actions.Select_layer_action(config.layers[i].id, true).do();
_this.save_action(params, true);
}
new app.Actions.Select_layer_action(active_layer, true).do();
}
else {
_this.save_action(params);
}
},
};
this.POP.show(settings);
document.getElementById("pop_data_name").select();
if (calc_size == true) {
//calc size once
this.save_dialog_onchange(true);
}
else{
this.save_dialog_onchange(false);
}
}
save_data_url() {
var max = 10 * 1000 * 1000;
if (config.WIDTH * config.WIDTH > 10 * 1000 * 1000) {
alertify.error('Size is too big, max ' + this.Helper.number_format(max, 0) + ' pixels.');
return;
}
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
this.disable_canvas_smooth(ctx);
//ask data
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
var data_url = canvas.toDataURL();
max = 1000 * 1000;
if (data_url.length > max) {
alertify.error('Size is too big, max ' + this.Helper.number_format(max, 0) + ' bytes.');
return;
}
var settings = {
title: 'Data URL',
params: [
{name: "url", title: "URL:", type: "textarea", value: data_url},
],
};
this.POP.show(settings);
}
update_file_size(file_size) {
if (typeof file_size == 'string') {
document.getElementById('file_size').innerHTML = file_size;
return;
}
if (file_size > 1024 * 1024)
file_size = this.Helper.number_format(file_size / 1024 / 1024, 2) + ' MB';
else if (file_size > 1024)
file_size = this.Helper.number_format(file_size / 1024, 2) + ' KB';
else
file_size = (file_size) + ' B';
document.getElementById('file_size').innerHTML = file_size;
}
/**
* /activated on save dialog parameters change - used for calculating file size
*
* @param {boolean} calculate_file_size
*/
save_dialog_onchange(calculate_file_size) {
var _this = this;
var user_response = this.POP.get_params();
var quality = parseInt(user_response.quality);
if (quality > 100 || quality < 1 || isNaN(quality) == true)
quality = 90;
quality = quality / 100;
//detect type
var type = user_response.type;
var parts = type.split(" ");
type = parts[0];
if (type == 'JPG' || type == 'WEBP')
document.getElementById('popup-tr-quality').style.display = '';
else
document.getElementById('popup-tr-quality').style.display = 'none';
if (type == 'GIF')
document.getElementById('popup-tr-delay').style.display = '';
else
document.getElementById('popup-tr-delay').style.display = 'none';
if (type == 'JSON' || type == 'GIF' || type == 'TIFF_LAYERS' || type == 'TIFF_CMYK' || type == 'PDF_CMYK')
document.getElementById('popup-tr-layers').style.display = 'none';
else
document.getElementById('popup-tr-layers').style.display = '';
if (user_response.layers == 'Separated')
document.getElementById('pop_data_name').disabled = true;
else
document.getElementById('pop_data_name').disabled = false;
if (user_response.layers == 'Separated (original types)') {
if(document.getElementById('popup-group-type')) {
document.getElementById('popup-group-type').style.opacity = "0.5";
}
document.getElementById('popup-tr-quality').style.display = '';
}
else {
if(document.getElementById('popup-group-type')) {
document.getElementById('popup-group-type').style.opacity = "1";
}
}
if(calculate_file_size == false){
return;
}
this.update_file_size('...');
if (user_response.calc_size == false || user_response.layers == 'Separated'
|| user_response.layers == 'Separated (original types)') {
document.getElementById('file_size').innerHTML = '-';
return;
}
if (type != 'JSON') {
//create temp canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
this.disable_canvas_smooth(ctx);
//ask data
if (user_response.layers == 'Selected' && type != 'GIF' && config.layer.type != null) {
//only current layer !!!
var layer = config.layer;
var initial_x = null;
var initial_y = null;
if (layer.x != null && layer.y != null && layer.width != null && layer.height != null) {
//change position to top left corner
initial_x = layer.x;
initial_y = layer.y;
layer.x = 0;
layer.y = 0;
canvas.width = layer.width;
canvas.height = layer.height;
}
this.Base_layers.convert_layers_to_canvas(ctx, layer.id, false);
if (initial_x != null) {
//restore position
layer.x = initial_x;
layer.y = initial_y;
}
}
else {
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
}
}
if (type != 'JSON' && (type == 'JPG' || config.TRANSPARENCY == false)) {
//add white background
ctx.globalCompositeOperation = 'destination-over';
this.fillCanvasBackground(ctx, '#ffffff');
ctx.globalCompositeOperation = 'source-over';
}
//calc size
if (type == 'PNG') {
//png
canvas.toBlob(function (blob) {
_this.update_file_size(blob.size);
});
}
else if (type == 'JPG') {
//jpg
canvas.toBlob(function (blob) {
_this.update_file_size(blob.size);
}, "image/jpeg", quality);
}
else if (type == 'WEBP') {
//WEBP
var data_header = "image/webp";
//check support
if (this.check_format_support(canvas, data_header, false) == false) {
this.update_file_size('-');
return;
}
canvas.toBlob(function (blob) {
_this.update_file_size(blob.size);
}, data_header, quality);
}
else if (type == 'AVIF') {
//AVIF
var data_header = "image/avif";
//check support
if (this.check_format_support(canvas, data_header, false) == false) {
this.update_file_size('-');
return;
}
canvas.toBlob(function (blob) {
_this.update_file_size(blob.size);
}, data_header, quality);
}
else if (type == 'BMP') {
//bmp
var data_header = "image/bmp";
//check support
if (this.check_format_support(canvas, data_header, false) == false) {
this.update_file_size('-');
return;
}
canvas.toBlob(function (blob) {
_this.update_file_size(blob.size);
}, data_header);
}
else if (type == 'TIFF') {
CanvasToTIFF.toBlob(canvas, function(blob) {
_this.update_file_size(blob.size);
}, {});
}
else if (type == 'TIFF_CMYK' || type == 'TIFF_LAYERS') {
// size estimate: W*H*4 bytes of pixel data + small overhead
_this.update_file_size(config.WIDTH * config.HEIGHT * 4 + 512);
}
else if (type == 'PDF' || type == 'PDF_CMYK') {
_this.update_file_size('-');
}
else if (type == 'JSON') {
//json
var data_json = this.export_as_json();
var blob = new Blob([data_json], {type: "text/plain"});
this.update_file_size(blob.size);
}
else if (type == 'GIF') {
//gif
this.update_file_size('-');
}
}
/**
* saves data in requested way
*
* @param {object} user_response parameters
* @param {boolean} autoname if use name from layer, false by default
*/
save_action(user_response, autoname) {
var fname = user_response.name;
if(autoname === true && user_response.layers == 'Selected'){
fname = config.layer.name;
}
var quality = parseInt(user_response.quality);
if (quality > 100 || quality < 1 || isNaN(quality) == true)
quality = 90;
quality = quality / 100;
var delay = parseInt(user_response.delay);
if (delay < 0 || isNaN(delay) == true)
delay = 400;
//detect type
var type = user_response.type;
var parts = type.split(" ");
type = parts[0];
//detect type from file name
for(var i in this.SAVE_TYPES) {
if (this.Helper.strpos(fname, '.' + i.toLowerCase()) !== false) {
type = i;
}
}
//save default type as cookie
if(this.Helper.getCookie('save_default') == '' || this.Helper.getCookie('save_default') != type){
this.Helper.setCookie('save_default', type);
}
if (type != 'JSON') {
//temp canvas
var canvas;
var ctx;
//get data
if (user_response.layers == 'Selected' && type != 'GIF') {
canvas = this.Base_layers.convert_layer_to_canvas();
ctx = canvas.getContext("2d");
}
else {
canvas = document.createElement('canvas');
ctx = canvas.getContext("2d");
canvas.width = config.WIDTH;
canvas.height = config.HEIGHT;
this.disable_canvas_smooth(ctx);
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
}
}
// CMYK and JPG need an opaque white background (no alpha channel in output)
if (type != 'JSON' && (type == 'JPG' || type == 'TIFF_CMYK' || config.TRANSPARENCY == false)) {
ctx.globalCompositeOperation = 'destination-over';
this.fillCanvasBackground(ctx, '#ffffff');
ctx.globalCompositeOperation = 'source-over';
}
if (type == 'PNG') {
//png - default format
if (this.Helper.strpos(fname, '.png') == false)
fname = fname + ".png";
//simple save example
//var link = document.createElement('a');
//link.download = fname;
//link.href = canvas.toDataURL();
//link.click();
//save using lib
canvas.toBlob(function (blob) {
filesaver.saveAs(blob, fname);
});
}
else if (type == 'JPG') {
//jpg
if (this.Helper.strpos(fname, '.jpg') == false)
fname = fname + ".jpg";
canvas.toBlob(function (blob) {
filesaver.saveAs(blob, fname);
}, "image/jpeg", quality);
}
else if (type == 'WEBP') {
//WEBP
if (this.Helper.strpos(fname, '.webp') == false)
fname = fname + ".webp";
var data_header = "image/webp";
//check support
if (this.check_format_support(canvas, data_header) == false)
return false;
canvas.toBlob(function (blob) {
filesaver.saveAs(blob, fname);
}, data_header, quality);
}
else if (type == 'AVIF') {
//AVIF
if (this.Helper.strpos(fname, '.avif') == false)
fname = fname + ".avif";
var data_header = "image/avif";
//check support
if (this.check_format_support(canvas, data_header) == false)
return false;
canvas.toBlob(function (blob) {
filesaver.saveAs(blob, fname);
}, data_header, quality);
}
else if (type == 'BMP') {
//bmp
if (this.Helper.strpos(fname, '.bmp') == false)
fname = fname + ".bmp";
var data_header = "image/bmp";
//check support
if (this.check_format_support(canvas, data_header) == false)
return false;
canvas.toBlob(function (blob) {
filesaver.saveAs(blob, fname);
}, data_header);
}
else if (type == 'TIFF') {
//tiff - single page RGBA (existing behaviour)
if (this.Helper.strpos(fname, '.tiff') == false)
fname = fname + ".tiff";
CanvasToTIFF.toBlob(canvas, function(blob) {
filesaver.saveAs(blob, fname);
}, {});
}
else if (type == 'TIFF_CMYK') {
//tiff - single page CMYK, print-ready
if (this.Helper.strpos(fname, '.tiff') == false)
fname = fname + ".tiff";
var resolution = this.Tools_settings.get_setting('resolution') || 300;
TiffWriter.toCMYK(canvas, function(buf) {
filesaver.saveAs(new Blob([buf], {type: 'image/tiff'}), fname);
}, {dpi: resolution});
}
else if (type == 'TIFF_LAYERS') {
//tiff - multipage: one IFD per visible layer
if (this.Helper.strpos(fname, '.tiff') == false)
fname = fname + ".tiff";
var resolution = this.Tools_settings.get_setting('resolution') || 300;
var layerCanvases = this._collect_layer_canvases();
TiffWriter.toMultipageRGBA(layerCanvases, function(buf) {
filesaver.saveAs(new Blob([buf], {type: 'image/tiff'}), fname);
}, {dpi: resolution});
}
else if (type == 'PDF') {
//pdf - RGB, one page per visible layer
if (this.Helper.strpos(fname, '.pdf') == false)
fname = fname + ".pdf";
var resolution = this.Tools_settings.get_setting('resolution') || 300;
var quality_val = quality;
var pdfCanvases;
if (user_response.layers == 'Selected') {
pdfCanvases = [canvas];
} else {
pdfCanvases = this._collect_layer_canvases();
}
PdfWriter.fromCanvases(pdfCanvases, {colorMode: 'rgb', quality: quality_val, dpi: resolution})
.then(function(blob) { filesaver.saveAs(blob, fname); });
}
else if (type == 'PDF_CMYK') {
//pdf - CMYK, one page per visible layer, print-ready
if (this.Helper.strpos(fname, '.pdf') == false)
fname = fname + ".pdf";
var resolution = this.Tools_settings.get_setting('resolution') || 300;
var layerCanvases = this._collect_layer_canvases();
PdfWriter.fromCanvases(layerCanvases, {colorMode: 'cmyk', dpi: resolution})
.then(function(blob) { filesaver.saveAs(blob, fname); });
}
else if (type == 'JSON') {
//json - full data with layers
if (this.Helper.strpos(fname, '.json') == false)
fname = fname + ".json";
var data_json = this.export_as_json();
var blob = new Blob([data_json], {type: "text/plain"});
//var data = window.URL.createObjectURL(blob); //html5
filesaver.saveAs(blob, fname);
}
else if (type == 'GIF') {
//gif
var cores = navigator.hardwareConcurrency || 4;
var gif_settings = {
workers: cores,
quality: 10, //1-30, lower is better
repeat: 0,
width: config.WIDTH,
height: config.HEIGHT,
dither: 'FloydSteinberg-serpentine',
workerScript: './src/js/libs/gifjs/gif.worker.js',
};
if (config.TRANSPARENCY == true) {
gif_settings.transparent = 'rgba(0,0,0,0)';
}
var gif = new GIF(gif_settings);
//add frames
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].visible == false)
continue;
ctx.clearRect(0, 0, config.WIDTH, config.HEIGHT);
if (config.TRANSPARENCY == false) {
this.fillCanvasBackground(ctx, '#ffffff');
}
this.Base_layers.convert_layers_to_canvas(ctx, config.layers[i].id, false);
gif.addFrame(ctx, {copy: true, delay: delay});
}
gif.render();
gif.on('finished', function (blob) {
filesaver.saveAs(blob, fname);
});
}
}
fillCanvasBackground(ctx, color, width = config.WIDTH, height = config.HEIGHT) {
ctx.beginPath();
ctx.rect(0, 0, width, height);
ctx.fillStyle = color;
ctx.fill();
}
check_format_support(canvas, data_header, show_error) {
var data = canvas.toDataURL(data_header);
var actualType = data.replace(/^data:([^;]*).*/, '$1');
if (data_header != actualType && data_header != "text/plain") {
if (show_error == undefined || show_error == true) {
//error - no support
alertify.error('Your browser does not support this format.');
}
return false;
}
return true;
}
/**
* exports all layers to JSON
*/
export_as_json() {
//get date
var today = new Date();
var yyyy = today.getFullYear();
var mm = today.getMonth() + 1; //January is 0!
var dd = today.getDate();
if (dd < 10)
dd = '0' + dd;
if (mm < 10)
mm = '0' + mm;
var today = yyyy + '-' + mm + '-' + dd;
//data
var export_data = {};
export_data.info = {
width: config.WIDTH,
height: config.HEIGHT,
about: 'Image data with multi-layers. Can be opened using miniPaint - '
+ 'https://github.com/viliusle/miniPaint',
date: today,
version: VERSION,
layer_active: config.layer.id,
guides: config.guides,
};
//fonts
export_data.user_fonts = config.user_fonts;
//layers
export_data.layers = [];
for (var i in config.layers) {
var layer = {};
for (var j in config.layers[i]) {
if (j[0] == '_' || j == 'link_canvas') {
//private data
continue;
}
layer[j] = config.layers[i][j];
}
export_data.layers.push(layer);
}
//image data
export_data.data = [];
for (var i in config.layers) {
if (config.layers[i].type != 'image')
continue;
var canvas = document.createElement('canvas');
canvas.width = config.layers[i].width_original;
canvas.height = config.layers[i].height_original;
this.disable_canvas_smooth(canvas.getContext("2d"));
canvas.getContext('2d').drawImage(config.layers[i].link, 0, 0);
var data_tmp = canvas.toDataURL("image/png");
export_data.data.push(
{
id: config.layers[i].id,
data: data_tmp,
}
);
canvas.width = 1;
canvas.height = 1;
}
return JSON.stringify(export_data, null, "\t");
}
/**
* Returns one canvas per visible layer, each composited individually.
* Used for multilayer TIFF and multipage PDF export.
*/
_collect_layer_canvases() {
var canvases = [];
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].visible == false) continue;
var c = document.createElement('canvas');
var cx = c.getContext('2d');
c.width = config.WIDTH;
c.height = config.HEIGHT;
this.disable_canvas_smooth(cx);
this.Base_layers.convert_layers_to_canvas(cx, config.layers[i].id, false);
canvases.push(c);
}
// Fall back to full composite if no layers found
if (canvases.length === 0) {
var c = document.createElement('canvas');
var cx = c.getContext('2d');
c.width = config.WIDTH;
c.height = config.HEIGHT;
this.disable_canvas_smooth(cx);
this.Base_layers.convert_layers_to_canvas(cx, null, false);
canvases.push(c);
}
return canvases;
}
/**
* removes smoothing, because it look ugly during zoom
*
* @param {ctx} ctx
*/
disable_canvas_smooth(ctx) {
ctx.webkitImageSmoothingEnabled = false;
ctx.oImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
}
}
export default File_save_class;
@@ -0,0 +1,142 @@
/**
* Outpaint / Expand Canvas — remote provider fills the new region.
* Menu target: generate/outpaint.outpaint
*/
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';
import apiService from './../../services/api.js';
import { getCapabilities } from './../../api/capabilities.js';
var instance = null;
class Generate_outpaint_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
async outpaint() {
var caps = await getCapabilities();
if (!caps.remote || !caps.remote.healthy) {
alertify.error(
'Expand Canvas requires a remote AI provider. ' +
'Set AI_PROVIDER (openai / invokeai / comfyui) in .env and restart.'
);
return;
}
var _this = this;
this.Dialog.show({
title: 'Expand Canvas (Outpaint)',
params: [
{
name: 'direction',
title: 'Expand direction:',
value: 'right',
values: ['right', 'left', 'bottom', 'top'],
},
{
name: 'size',
title: 'Pixels to add:',
type: 'range',
value: 256,
range: [64, 1024],
step: 64,
},
{
name: 'prompt',
title: 'Describe the expansion (optional):',
value: '',
placeholder: "e.g. 'continue the landscape', 'more sky and clouds'",
},
],
on_finish: async function (params) {
await _this._run(params);
},
});
}
async _run(params) {
if (this.isProcessing) return;
if (config.layer.type !== 'image') {
alertify.error('Current layer must be an image.');
return;
}
this.isProcessing = true;
alertify.message('Expanding canvas... please wait', 0);
try {
var layerCanvas = document.createElement('canvas');
layerCanvas.width = config.layer.width_original;
layerCanvas.height = config.layer.height_original;
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
var response = await fetch(
(window.API_BASE_URL || '') + '/api/generate/outpaint',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageB64,
direction: params.direction,
size: params.size || 256,
prompt: params.prompt || '',
}),
}
);
if (!response.ok) {
var err = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(err.detail || 'Outpaint failed');
}
var result = await response.json();
var img = new Image();
img.onload = () => {
var newW = img.naturalWidth;
var newH = img.naturalHeight;
var resultCanvas = document.createElement('canvas');
resultCanvas.width = newW;
resultCanvas.height = newH;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
// Update canvas dimensions and replace layer
config.WIDTH = newW;
config.HEIGHT = newH;
app.State.do_action(
new app.Actions.Bundle_action('outpaint', 'Expand Canvas', [
new app.Actions.Resize_canvas_action(newW, newH),
new app.Actions.Update_layer_image_action(resultCanvas),
])
);
alertify.dismissAll();
alertify.success('Canvas expanded!');
this.isProcessing = false;
};
img.onerror = () => {
alertify.dismissAll();
alertify.error('Failed to load expanded image.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
alertify.dismissAll();
alertify.error('Outpaint failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
export default Generate_outpaint_class;
@@ -0,0 +1,216 @@
/**
* Text → Image — generates via remote or local-GPU provider,
* pastes result as a new layer on the current canvas.
*
* Menu target: generate/text_to_image.text_to_image
*/
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';
import apiService from './../../services/api.js';
import { getCapabilities } from './../../api/capabilities.js';
import { showProgress, updateProgress, hideProgress, connectProgressSSE, disconnectProgressSSE } from './../../libs/progress_overlay.js';
var instance = null;
class Generate_text_to_image_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
async text_to_image() {
var caps = await getCapabilities();
var hasRemote = caps.remote && caps.remote.healthy;
var hasLocal = caps.local && caps.local.local_gpu_available;
if (!hasRemote && !hasLocal) {
alertify.error(
'Text → Image requires an AI provider. ' +
'Set AI_PROVIDER=openai / invokeai / comfyui / local_gpu in .env and restart, ' +
'or configure one in Image → AI Provider Settings.'
);
return;
}
var _this = this;
var canvasW = config.WIDTH || 1024;
var canvasH = config.HEIGHT || 1024;
// Build provider info line
var providerHtml = hasRemote
? `<span style="color:#44cc44">● ${caps.remote.provider}</span>`
: `<span style="color:#44cc44">● local GPU · ${caps.local.gpu_tier || ''} · ${_shortGpu(caps.local.gpu_device)}</span>`;
// Model note for local GPU
var modelNote = '';
if (hasLocal && !hasRemote) {
var rec = caps.local.local_gpu_capabilities && caps.local.local_gpu_capabilities.recommended;
var m = rec && rec.txt2img;
if (m) {
modelNote = `Model: <span style="color:#ddd">${m.model_id.split('/').pop()}</span>`;
if (m.memory_opt && m.memory_opt !== 'none') modelNote += ` · <span style="color:#aaa">${m.memory_opt}</span>`;
}
}
// Estimate generation time (rough guide for the progress bar)
var estSec = hasLocal ? 60 : 15; // local GPU ~1 min; OpenAI ~15s
var defaultW = Math.min(canvasW, hasLocal ? (caps.local.local_gpu_capabilities?.recommended?.txt2img?.native_res || 1024) : 1024);
var defaultH = Math.min(canvasH, defaultW);
this.Dialog.show({
title: 'Text → Image',
params: [
{
title: '',
html: `<div style="font-size:11px;margin:0 0 8px">
Provider: ${providerHtml}${modelNote ? ' · ' + modelNote : ''}<br>
<span style="color:#777">Generation typically takes ${estSec < 30 ? 'a few seconds' : estSec < 90 ? '3090 seconds on local GPU' : '13 minutes on local GPU'}.</span>
</div>`,
},
{
name: 'prompt',
title: 'Describe your image:',
type: 'textarea',
value: '',
placeholder: "e.g. 'a serene mountain lake at sunset, cinematic lighting'",
},
{
name: 'negative_prompt',
title: 'Avoid (optional):',
value: '',
placeholder: 'blurry, distorted, watermark',
},
{
name: 'width',
title: 'Width (px):',
value: defaultW,
range: [256, 2048],
step: 64,
type: 'range',
},
{
name: 'height',
title: 'Height (px):',
value: defaultH,
range: [256, 2048],
step: 64,
type: 'range',
},
{
name: 'placement',
title: 'Add as:',
value: 'new_layer',
values: ['new_layer', 'replace_canvas'],
},
{
name: 'steps',
title: 'Steps:',
type: 'range',
value: 30,
range: [10, 60],
step: 5,
},
{
name: 'seed',
title: 'Seed (0 = random):',
value: 0,
range: [0, 2147483647],
step: 1,
type: 'range',
},
],
on_finish: async function (params) {
if (!params.prompt || !params.prompt.trim()) {
alertify.warning('Please enter a description.');
return;
}
await _this._generate(params, estSec);
},
});
}
async _generate(params, estSec) {
if (this.isProcessing) return;
this.isProcessing = true;
connectProgressSSE('txt2img', window.API_BASE_URL || '');
showProgress('Generating image…', estSec || 60);
try {
var result = await apiService.textToImage(params.prompt, {
width: params.width || 1024,
height: params.height || 1024,
negativePrompt: params.negative_prompt || '',
steps: params.steps || 30,
seed: params.seed || 0,
});
updateProgress(95, 'Placing image…');
var img = new Image();
img.onload = () => {
if (params.placement === 'replace_canvas') {
config.WIDTH = img.naturalWidth;
config.HEIGHT = img.naturalHeight;
var resultCanvas = document.createElement('canvas');
resultCanvas.width = img.naturalWidth;
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
app.State.do_action(
new app.Actions.Bundle_action('txt2img_replace', 'Text → Image', [
new app.Actions.Update_layer_image_action(resultCanvas)
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('txt2img_layer', 'Text → Image Layer', [
new app.Actions.Insert_layer_action({
name: params.prompt.slice(0, 30),
type: 'image',
data: img.src,
x: 0, y: 0,
width: img.naturalWidth,
height: img.naturalHeight,
width_original: img.naturalWidth,
height_original: img.naturalHeight,
})
])
);
}
disconnectProgressSSE();
hideProgress();
alertify.success('Image generated!');
this.isProcessing = false;
};
img.onerror = () => {
disconnectProgressSSE();
hideProgress();
alertify.error('Failed to load generated image.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
disconnectProgressSSE();
hideProgress();
alertify.error('Generation failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
function _shortGpu(name) {
if (!name) return 'GPU';
return name.replace(/^NVIDIA GeForce /i, '').replace(/^NVIDIA /i, '');
}
export default Generate_text_to_image_class;
@@ -0,0 +1,36 @@
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
class Help_about_class {
constructor() {
this.POP = new Dialog_class();
}
//about
about() {
var email = 'www.viliusl@gmail.com';
var settings = {
title: 'About',
params: [
{title: "", html: '<img style="width:64px;" class="about-logo" alt="" src="images/logo-colors.png" />'},
{title: "Name:", html: '<span class="about-name">PaintPlus</span>'},
{title: "Version:", value: VERSION},
{title: "Description:", value: "Layer-based image editor with AI tools."},
{title: "", html: '<hr style="margin:8px 0;border-color:#444;">'},
{title: "Base:", html: '<a href="https://github.com/viliusle/miniPaint" target="_blank">miniPaint</a> by ViliusL'},
{title: "AI Erase:", html: 'LaMa (Samsung Research) via <a href="https://github.com/enesmsahin/simple-lama-inpainting" target="_blank">simple-lama-inpainting</a>'},
{title: "Bg Removal:", html: '<a href="https://github.com/danielgatis/rembg" target="_blank">rembg</a> / U2Net / OpenCV'},
{title: "Smart Select:", html: '<a href="https://github.com/facebookresearch/segment-anything" target="_blank">SAM</a> (Meta AI)'},
{title: "Remote AI:", html: 'InvokeAI · ComfyUI · OpenAI (user-configured)'},
{title: "", html: '<hr style="margin:8px 0;border-color:#444;">'},
{title: "GitHub:", html: '<a href="https://github.com/outis1one/EditmaskwithAI" target="_blank">outis1one/EditmaskwithAI</a>'},
],
};
this.POP.show(settings);
}
}
export default Help_about_class;
@@ -0,0 +1,44 @@
import Dialog_class from './../../libs/popup.js';
class Help_shortcuts_class {
constructor() {
this.POP = new Dialog_class();
}
//shortcuts
shortcuts() {
var settings = {
title: 'Keyboard Shortcuts',
className: 'shortcuts',
params: [
{title: "F", value: 'Auto Adjust Colors'},
{title: "F3 / &#8984; + F", value: 'Search'},
{title: "Ctrl + C", value: 'Copy to Clipboard'},
{title: "D", value: 'Duplicate'},
{title: "S", value: 'Export'},
{title: "G", value: 'Grid on/off'},
{title: "I", value: 'Information'},
{title: "N", value: 'New layer'},
{title: "O", value: 'Open'},
{title: "CTRL + V", value: 'Paste'},
{title: "F10", value: 'Quick Load'},
{title: "F9", value: 'Quick Save'},
{title: "R", value: 'Resize'},
{title: "L", value: 'Rotate left'},
{title: "U", value: 'Ruler'},
{title: "Shift + S", value: 'Save As'},
{title: "CTRL + A", value: 'Select All'},
{title: "H", value: 'Shapes'},
{title: "T", value: 'Trim'},
{title: "CTRL + Z", value: 'Undo'},
{title: "Scroll up", value: 'Zoom in'},
{title: "Scroll down", value: 'Zoom out'},
],
};
this.POP.show(settings);
}
}
export default Help_shortcuts_class;
@@ -0,0 +1,168 @@
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';
var instance = null;
class Image_autoAdjust_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.POP = new Dialog_class();
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 == 70 && event.ctrlKey != true && event.metaKey != true) {
//F - adjust
this.auto_adjust();
event.preventDefault();
}
}, false);
}
auto_adjust() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.get_adjust_data(img);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
get_adjust_data(data) {
//settings
var white = 240; //white color min
var black = 30; //black color max
var target_white = 1; //how much % white colors should take
var target_black = 0.5; //how much % black colors should take
var modify = 1.1; //color modify strength
var cycles_count = 10; //how much iteration to change colors
var imgData = data.data;
var W = data.width;
var H = data.height;
var n = 0; //pixels count without transparent
//make sure we have white
var n_valid = 0;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
if ((imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3 > white)
n_valid++;
n++;
}
var target = target_white;
var n_fix_white = 0;
var done = false;
for (var j = 0; j < cycles_count; j++) {
if (n_valid * 100 / n >= target)
done = true;
if (done == true)
break;
n_fix_white++;
//adjust
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
for (var c = 0; c < 3; c++) {
var x = i + c;
if (imgData[x] < 10)
continue;
//increase white
imgData[x] *= modify;
imgData[x] = Math.round(imgData[x]);
if (imgData[x] > 255)
imgData[x] = 255;
}
}
//recheck
n_valid = 0;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
if ((imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3 > white)
n_valid++;
}
}
//make sure we have black
n_valid = 0;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
if ((imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3 < black)
n_valid++;
}
target = target_black;
var n_fix_black = 0;
var done = false;
for (var j = 0; j < cycles_count; j++) {
if (n_valid * 100 / n >= target)
done = true;
if (done == true)
break;
n_fix_black++;
//adjust
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
for (var c = 0; c < 3; c++) {
var x = i + c;
if (imgData[x] > 240)
continue;
//increase black
imgData[x] -= (255 - imgData[x]) * modify - (255 - imgData[x]);
imgData[x] = Math.round(imgData[x]);
}
}
//recheck
n_valid = 0;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
if ((imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3 < black)
n_valid++;
}
}
//log('Iterations: brighten='+n_fix_white+", darken="+n_fix_black);
return data;
}
}
export default Image_autoAdjust_class;
@@ -0,0 +1,124 @@
/**
* Auto-Enhance — one-click smart photo improvement.
* Applies auto white balance, CLAHE contrast, saturation boost, and mild sharpening.
* Strength slider lets the user dial in how strong the effect is.
*
* Menu target: image/auto_enhance.auto_enhance
*/
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
class Image_auto_enhance_class {
constructor() {
if (instance) return instance;
instance = this;
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
async auto_enhance() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
var _this = this;
this.Dialog.show({
title: 'Auto-Enhance',
params: [
{
title: '',
html: `<div style="font-size:11px;color:#888;margin-bottom:8px;">
Automatically improves white balance, contrast, saturation, and sharpness.
</div>`,
},
{
name: 'strength',
title: 'Strength:',
value: '100',
values: ['25', '50', '75', '100'],
type: 'select',
},
{
name: 'new_layer',
title: 'Keep original as separate layer:',
value: false,
},
],
on_finish: async function (params) {
await _this._run(parseFloat(params.strength) / 100, params.new_layer);
},
});
}
async _run(strength, newLayer) {
if (this.isProcessing) return;
this.isProcessing = true;
alertify.message('Enhancing…', 0);
try {
const layer = config.layer;
const c = document.createElement('canvas');
c.width = layer.width_original; c.height = layer.height_original;
c.getContext('2d').drawImage(layer.link, 0, 0);
const imageB64 = c.toDataURL('image/png').split(',')[1];
const base = window.API_BASE_URL || '';
const r = await fetch(`${base}/api/enhance`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageB64, strength }),
});
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || 'Failed');
const data = await r.json();
const img = new Image();
img.onload = () => {
const rc = document.createElement('canvas');
rc.width = img.naturalWidth; rc.height = img.naturalHeight;
rc.getContext('2d').drawImage(img, 0, 0);
if (newLayer) {
app.State.do_action(
new app.Actions.Bundle_action('auto_enhance', 'Auto-Enhance', [
new app.Actions.Insert_layer_action({
name: layer.name + ' (Enhanced)',
type: 'image',
data: img.src,
x: layer.x, y: layer.y,
width: img.naturalWidth, height: img.naturalHeight,
width_original: img.naturalWidth, height_original: img.naturalHeight,
})
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('auto_enhance', 'Auto-Enhance', [
new app.Actions.Update_layer_image_action(rc)
])
);
}
alertify.dismissAll();
alertify.success('Enhancement applied.');
this.isProcessing = false;
};
img.onerror = () => {
alertify.dismissAll();
alertify.error('Failed to load result.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + data.result;
} catch (err) {
alertify.dismissAll();
alertify.error('Auto-enhance failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
export default Image_auto_enhance_class;
@@ -0,0 +1,134 @@
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 ImageFilters_class from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Image_colorCorrections_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.ImageFilters = ImageFilters_class;
}
color_corrections() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Color Corrections',
preview: true,
on_change: function (params, canvas_preview, w, h, canvas) {
//destructive effects
var img = this.layer_active_small_ctx.getImageData(0, 0, w, h);
var data = _this.do_corrections(img, params, false);
canvas_preview.putImageData(data, 0, 0);
//non-destructive
canvas_preview.filter = "brightness(" + (1 + (params.param_b / 100)) + ")";
canvas_preview.filter += " contrast(" + (1 + (params.param_c / 100)) + ")";
canvas_preview.filter += " saturate(" + (1 + (params.param_s / 100)) + ")";
canvas_preview.filter += " hue-rotate(" + params.param_h + "deg)";
canvas_preview.drawImage(canvas, 0, 0);
},
params: [
{name: "param_b", title: "Brightness:", value: "0", range: [-100, 100]},
{name: "param_c", title: "Contrast:", value: "0", range: [-100, 100]},
{name: "param_s", title: "Saturation:", value: "0", range: [-100, 100]},
{name: "param_h", title: "Hue:", value: "0", range: [-180, 180]},
{},
{name: "param_l", title: "Luminance:", value: "0", range: [-100, 100]},
{},
{name: "param_red", title: "Red channel:", value: "0", range: [-255, 255]},
{name: "param_green", title: "Green channel:", value: "0", range: [-255, 255]},
{name: "param_blue", title: "Blue channel:", value: "0", range: [-255, 255]},
],
on_finish: function (params) {
_this.save_changes(params);
},
};
this.POP.show(settings);
}
save_changes(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.do_corrections(img, params);
ctx.putImageData(data, 0, 0);
//save
app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
//non-destructive filters
//multiple do_action() + do_corrections() does not work together yet.
if(params.param_b != 0) {
var parameters = {value: params.param_b};
var filter_id = null;
app.State.do_action(
new app.Actions.Add_layer_filter_action(null, 'brightness', parameters, filter_id)
);
}
if(params.param_c != 0) {
var parameters = {value: params.param_c};
var filter_id = null;
app.State.do_action(
new app.Actions.Add_layer_filter_action(null, 'contrast', parameters, filter_id)
);
}
if(params.param_s != 0) {
var parameters = {value: params.param_s};
var filter_id = null;
app.State.do_action(
new app.Actions.Add_layer_filter_action(null, 'saturate', parameters, filter_id)
);
}
if(params.param_h != 0) {
var parameters = {value: params.param_h};
var filter_id = null;
app.State.do_action(
new app.Actions.Add_layer_filter_action(null, 'hue-rotate', parameters, filter_id)
);
}
}
/**
* corrections (destructive)
*
* @param data
* @param params
* @returns {*}
*/
do_corrections(data, params) {
//luminance
if(params.param_l != 0) {
var data = this.ImageFilters.HSLAdjustment(data, 0, 0, params.param_l);
}
//RGB corrections
if(params.param_red != 0 || params.param_green != 0 || params.param_blue != 0) {
var data = this.ImageFilters.ColorTransformFilter(data, 1, 1, 1, 1,
params.param_red, params.param_green, params.param_blue, 1);
}
return data;
}
}
export default Image_colorCorrections_class;
@@ -0,0 +1,114 @@
/**
* Color Palette Extractor — pull dominant colors from the current image layer.
* Shows a floating swatch panel; click a swatch to copy the hex or set as active color.
*
* Menu target: image/color_palette.color_palette
*/
import config from './../../config.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
class Image_color_palette_class {
constructor() {
if (instance) return instance;
instance = this;
this._panel = null;
}
async color_palette() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
// Toggle: if panel already showing, close it
if (this._panel) { this._removePanel(); return; }
alertify.message('Extracting colors…', 0);
try {
const layer = config.layer;
const c = document.createElement('canvas');
c.width = layer.width_original; c.height = layer.height_original;
c.getContext('2d').drawImage(layer.link, 0, 0);
const imageB64 = c.toDataURL('image/png').split(',')[1];
const base = window.API_BASE_URL || '';
const r = await fetch(`${base}/api/extract-colors`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageB64, count: 8 }),
});
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || 'Failed');
const data = await r.json();
alertify.dismissAll();
this._showPanel(data.colors);
} catch (err) {
alertify.dismissAll();
alertify.error('Color extraction failed: ' + (err.message || err));
}
}
_showPanel(colors) {
this._removePanel();
const panel = document.createElement('div');
panel.id = 'color_palette_panel';
Object.assign(panel.style, {
position: 'fixed', bottom: '72px', right: '24px',
background: '#1a1a1a', border: '1px solid #3a3a3a',
borderRadius: '12px', padding: '12px 14px',
zIndex: '9998', boxShadow: '0 6px 24px rgba(0,0,0,0.6)',
fontFamily: 'sans-serif', fontSize: '12px', color: '#bbb',
userSelect: 'none', minWidth: '180px',
});
const swatchesHtml = colors.map(hex => `
<div title="Click to copy • Shift+click to set active color"
data-hex="${hex}"
style="display:inline-block;width:32px;height:32px;border-radius:6px;
background:${hex};cursor:pointer;border:2px solid transparent;
transition:border-color .12s;margin:2px;"
onmouseover="this.style.borderColor='#fff'"
onmouseout="this.style.borderColor='transparent'">
</div>`).join('');
panel.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:12px;color:#888;">Image Palette</span>
<span id="cp-close" style="cursor:pointer;color:#666;font-size:16px;line-height:1;">×</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:2px;">${swatchesHtml}</div>
<div id="cp-copied" style="font-size:11px;color:#4ade80;margin-top:6px;min-height:14px;"></div>
<div style="font-size:10px;color:#555;margin-top:4px;">Click: copy hex · Shift+click: set color</div>`;
document.body.appendChild(panel);
this._panel = panel;
// Close button
panel.querySelector('#cp-close').addEventListener('click', () => this._removePanel());
// Swatch clicks
panel.querySelectorAll('[data-hex]').forEach(el => {
el.addEventListener('click', e => {
const hex = el.dataset.hex;
if (e.shiftKey) {
// Set as active color in miniPaint
config.COLOR = hex;
const copiedEl = panel.querySelector('#cp-copied');
if (copiedEl) copiedEl.textContent = `Active color set to ${hex}`;
} else {
navigator.clipboard.writeText(hex).catch(() => {});
const copiedEl = panel.querySelector('#cp-copied');
if (copiedEl) { copiedEl.textContent = `Copied ${hex}`; }
}
});
});
}
_removePanel() {
if (this._panel) { this._panel.remove(); this._panel = null; }
}
}
export default Image_color_palette_class;
@@ -0,0 +1,174 @@
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 ImageFilters_class from './../../libs/imagefilters.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
class Image_decreaseColors_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.ImageFilters = ImageFilters_class;
}
decrease_colors() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Decrease Color Depth',
preview: true,
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.get_decreased_data(img, params.colors, params.greyscale);
canvas_preview.putImageData(data, 0, 0);
},
params: [
{name: "colors", title: "Colors:", value: 10, range: [1, 256]},
{name: "greyscale", title: "Greyscale:", value: false},
],
on_finish: function (params) {
_this.execute(params);
},
};
this.POP.show(settings);
}
execute(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.get_decreased_data(img, params.colors, params.greyscale);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
get_decreased_data(data, colors, greyscale) {
var img = data.data;
var imgData = data.data;
var W = data.width;
var H = data.height;
var palette = [];
var block_size = 10;
//create tmp canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = W;
canvas.height = H;
//collect top colors
ctx.drawImage(config.layer.link, 0, 0, Math.ceil(W / block_size), Math.ceil(H / block_size));
var img_p = ctx.getImageData(0, 0, Math.ceil(W / block_size), Math.ceil(H / block_size));
var imgData_p = img_p.data;
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < imgData_p.length; i += 4) {
if (imgData_p[i + 3] == 0)
continue; //transparent
var grey = Math.round(0.2126 * imgData_p[i] + 0.7152 * imgData_p[i + 1]
+ 0.0722 * imgData_p[i + 2]);
palette.push([imgData_p[i], imgData_p[i + 1], imgData_p[i + 2], grey]);
}
//calculate weights
var grey_palette = [];
for (var i = 0; i < 256; i++)
grey_palette[i] = 0;
for (var i = 0; i < palette.length; i++)
grey_palette[palette[i][3]]++;
//remove similar colors
for (var max = 10 * 3; max < 100 * 3; max = max + 10 * 3) {
if (palette.length <= colors)
break;
for (var i = 0; i < palette.length; i++) {
if (palette.length <= colors)
break;
var valid = true;
for (var j = 0; j < palette.length; j++) {
if (palette.length <= colors)
break;
if (i == j)
continue;
if (Math.abs(palette[i][0] - palette[j][0])
+ Math.abs(palette[i][1] - palette[j][1])
+ Math.abs(palette[i][2] - palette[j][2]) < max) {
if (grey_palette[palette[i][3]] > grey_palette[palette[j][3]]) {
//remove color
palette.splice(j, 1);
j--;
}
else {
valid = false;
break;
}
}
}
//remove color
if (valid == false) {
palette.splice(i, 1);
i--;
}
}
}
palette = palette.slice(0, colors);
//change
var p_n = palette.length;
for (var j = 0; j < H; j++) {
for (var i = 0; i < W; i++) {
var k = ((j * (W * 4)) + (i * 4));
if (imgData[k + 3] == 0)
continue; //transparent
//find closest color
var index1 = 0;
var min = 999999;
var diff1;
for (var m = 0; m < p_n; m++) {
var diff = Math.abs(palette[m][0] - imgData[k])
+ Math.abs(palette[m][1] - imgData[k + 1])
+ Math.abs(palette[m][2] - imgData[k + 2]);
if (diff < min) {
min = diff;
index1 = m;
diff1 = diff;
}
}
imgData[k] = palette[index1][0];
imgData[k + 1] = palette[index1][1];
imgData[k + 2] = palette[index1][2];
if (greyscale == true) {
var mid = Math.round(0.2126 * imgData[k] + 0.7152 * imgData[k + 1]
+ 0.0722 * imgData[k + 2]);
imgData[k] = mid;
imgData[k + 1] = mid;
imgData[k + 2] = mid;
}
}
}
return data;
}
}
export default Image_decreaseColors_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 Image_flip_class {
constructor() {
this.Base_layers = new Base_layers_class();
}
vertical() {
this.flip('vertical');
}
horizontal() {
this.flip('horizontal');
}
flip(mode) {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//create destination canvas
var canvas2 = document.createElement('canvas');
canvas2.width = canvas.width;
canvas2.height = canvas.height;
var ctx2 = canvas2.getContext("2d");
canvas2.dataset.x = canvas.dataset.x;
canvas2.dataset.y = canvas.dataset.y;
//flip
if (mode == 'vertical') {
ctx2.scale(1, -1);
ctx2.drawImage(canvas, 0, canvas2.height * -1);
}
else if (mode == 'horizontal') {
ctx2.scale(-1, 1);
ctx2.drawImage(canvas, canvas2.width * -1, 0);
}
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas2)
);
}
}
export default Image_flip_class;
@@ -0,0 +1,229 @@
/**
* Fit to Frame — resize/extend/crop image to a standard print frame size.
*
* Modes:
* crop — center-crop to aspect ratio, scale to print resolution (no AI needed)
* extend — scale to fill one dimension, AI-outpaint the gap (needs provider)
* smart — auto-pick: extend if gap < 15% of dimension, else crop
*
* Menu target: image/frame_fit.frame_fit
*/
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';
import { getCapabilities } from './../../api/capabilities.js';
import { showProgress, hideProgress } from './../../libs/progress_overlay.js';
var instance = null;
const FRAME_SIZES = [
'4x6', '5x7', '8x10', '11x14', '16x20', '18x24', '20x24', '24x36',
'4x4', '8x8', '12x12',
];
// Pixels at 300 dpi for preview labels
const FRAME_PX = {
'4x6': [1200, 1800], '5x7': [1500, 2100],
'8x10': [2400, 3000], '11x14': [3300, 4200],
'16x20': [4800, 6000], '18x24': [5400, 7200],
'20x24': [6000, 7200], '24x36': [7200, 10800],
'4x4': [1200, 1200], '8x8': [2400, 2400], '12x12': [3600, 3600],
};
class Image_frame_fit_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
async frame_fit() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
var caps = await getCapabilities();
var hasRemote = caps.remote && caps.remote.healthy;
var _this = this;
var W = config.layer.width_original;
var H = config.layer.height_original;
// Build display labels with pixel sizes
var sizeLabels = FRAME_SIZES.map(s => {
var px = FRAME_PX[s] || [0, 0];
return `${s}" (${px[0]}×${px[1]}px @ 300dpi)`;
});
this.Dialog.show({
title: 'Fit to Frame',
params: [
{
title: '',
html: `<div style="font-size:11px;color:#888;margin:0 0 8px;">
Current image: ${W}×${H}px<br>
Crop = no AI needed. Extend = AI fills the gaps${hasRemote ? '' : ' <span style="color:#ffaa00">(no provider configured — extend will use mirror fill)</span>'}.
</div>`,
},
{
name: 'frame',
title: 'Frame size:',
value: sizeLabels[1], // default 5x7
values: sizeLabels,
type: 'select',
},
{
name: 'orientation',
title: 'Orientation:',
value: 'auto',
values: ['auto', 'portrait', 'landscape'],
type: 'select',
},
{
name: 'mode',
title: 'Fit mode:',
value: 'smart',
values: ['smart', 'crop', 'extend'],
type: 'select',
},
{
name: 'dpi',
title: 'Output DPI:',
value: '300',
values: ['72', '150', '200', '300'],
type: 'select',
},
{
name: 'prompt',
title: 'Extend prompt (optional):',
value: '',
placeholder: 'e.g. "continue the background naturally" — blank works well',
},
{
name: 'new_layer',
title: 'Result as new layer (keep original):',
value: true,
},
],
on_finish: async function (params) {
var frameKey = params.frame.split('"')[0]; // strip label suffix back to "8x10"
await _this._run(frameKey, params);
},
});
}
async _run(frameKey, params) {
if (this.isProcessing) return;
this.isProcessing = true;
var mode = params.mode || 'smart';
showProgress(
mode === 'extend'
? 'Fitting to frame with AI extension…'
: 'Fitting to frame…',
mode === 'extend' ? 45 : 5
);
try {
var layerCanvas = document.createElement('canvas');
layerCanvas.width = config.layer.width_original;
layerCanvas.height = config.layer.height_original;
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/frame-fit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageB64,
frame: frameKey,
orientation: params.orientation || 'auto',
mode: params.mode || 'smart',
dpi: parseInt(params.dpi) || 300,
prompt: params.prompt || '',
}),
});
if (!r.ok) {
var err = await r.json().catch(() => ({ detail: 'Server error' }));
throw new Error(err.detail || 'Frame fit failed');
}
var result = await r.json();
var img = new Image();
img.onload = () => {
var resultCanvas = document.createElement('canvas');
resultCanvas.width = img.naturalWidth;
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
var fitW = img.naturalWidth;
var fitH = img.naturalHeight;
if (params.new_layer) {
var dataURL = img.src;
app.State.do_action(
new app.Actions.Bundle_action('frame_fit_layer', 'Fit to Frame', [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: fitW,
HEIGHT: fitH,
}),
new app.Actions.Insert_layer_action({
name: `${frameKey} fit`,
type: 'image',
data: dataURL,
x: 0, y: 0,
width: fitW,
height: fitH,
width_original: fitW,
height_original: fitH,
}),
new app.Actions.Prepare_canvas_action('do'),
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('frame_fit', 'Fit to Frame', [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: fitW,
HEIGHT: fitH,
}),
new app.Actions.Update_layer_image_action(resultCanvas),
new app.Actions.Prepare_canvas_action('do'),
])
);
}
hideProgress();
alertify.success(
`Done! ${result.output_pixels.width}×${result.output_pixels.height}px` +
` (${result.frame} ${result.orientation}, ${result.mode_used})`
);
this.isProcessing = false;
};
img.onerror = () => {
hideProgress();
alertify.error('Failed to load result.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
hideProgress();
alertify.error('Frame fit failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
export default Image_frame_fit_class;
@@ -0,0 +1,122 @@
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 Image_histogram_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
}
histogram() {
var _this = this;
var settings = {
title: 'Histogram',
on_change: function (params) {
_this.histogram_onload(params);
},
params: [
{name: "channel", title: "Channel:", values: ["Gray", "Red", "Green", "Blue"], },
{title: 'Histogram:', function: function () {
var html = '<canvas style="position:relative;" id="c_h" width="256" height="100"></canvas>';
return html;
}},
{title: "Total pixels:", value: ""},
{title: "Average:", value: ""},
],
};
this.POP.show(settings);
this.histogram_onload({});
}
histogram_onload(params) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id);
var ctx = canvas.getContext("2d");
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var imgData = img.data;
var channel = 0;
if (params.channel == 'Red')
channel = 1;
else if (params.channel == 'Green')
channel = 2;
else if (params.channel == 'Blue')
channel = 3;
var hist_data = [[], [], [], []]; //grey, red, green, blue
var total = imgData.length / 4;
var sum = 0;
var grey;
for (var i = 0; i < imgData.length; i += 4) {
//collect grey
grey = Math.round((imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3);
sum = sum + imgData[i] + imgData[i + 1] + imgData[i + 2];
if (hist_data[0][grey] == undefined)
hist_data[0][grey] = 1;
else
hist_data[0][grey]++;
//collect colors
for (var c = 0; c < 3; c++) {
if (c + 1 != channel)
continue;
if (hist_data[c + 1][imgData[i + c]] == undefined)
hist_data[c + 1][imgData[i + c]] = 1;
else
hist_data[c + 1][imgData[i + c]]++;
}
}
var c = document.getElementById("c_h").getContext("2d");
c.rect(0, 0, 256, 100);
c.fillStyle = "#ffffff";
c.fill();
var opacity = 1;
//draw histogram
for (var h in hist_data) {
for (var i = 0; i <= 255; i++) {
if (h != channel)
continue;
if (hist_data[h][i] == 0)
continue;
c.beginPath();
if (h == 0)
c.strokeStyle = "rgba(64, 64, 64, " + opacity * 2 + ")";
else if (h == 1)
c.strokeStyle = "rgba(255, 0, 0, " + opacity + ")";
else if (h == 2)
c.strokeStyle = "rgba(0, 255, 0, " + opacity + ")";
else if (h == 3)
c.strokeStyle = "rgba(0, 0, 255, " + opacity + ")";
c.lineWidth = 1;
c.moveTo(i + 0.5, 100 + 0.5);
c.lineTo(i + 0.5, 100 - Math.round(hist_data[h][i] * 255 * 100 / total / 6) + 0.5);
c.stroke();
}
}
document.getElementById("pop_data_totalpixel").innerHTML = this.Helper.number_format(total, 0);
var average;
if (total > 0)
average = Math.round(sum * 10 / total / 3) / 10;
else
average = '-';
document.getElementById("pop_data_average").innerHTML = average;
canvas.width = 1;
canvas.height = 1;
}
}
export default Image_histogram_class;
@@ -0,0 +1,144 @@
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import Helper_class from './../../libs/helpers.js';
import Base_layers_class from './../../core/base-layers.js';
import Tools_settings_class from './../tools/settings.js';
var instance = null;
class Image_information_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.POP = new Dialog_class();
this.Helper = new Helper_class();
this.Tools_settings = new Tools_settings_class();
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.key.toLowerCase();
if (this.Helper.is_input(event.target))
return;
if (code == "i") {
this.information();
event.preventDefault();
}
}, false);
}
information() {
var _this = this;
var pixels = config.WIDTH * config.HEIGHT;
pixels = this.Helper.number_format(pixels, 0);
var units = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var width = this.Helper.get_user_unit(config.WIDTH, units, resolution);
var height = this.Helper.get_user_unit(config.HEIGHT, units, resolution);
var settings = {
title: 'Information',
params: [
{title: "Width:", value: width + ' ' + units},
{title: "Height:", value: height + ' ' + units},
{title: "Pixels:", value: pixels},
{title: "Layers:", value: config.layers.length},
{title: "Unique colors:", value: '...'},
],
};
if(units != 'pixels'){
settings.params[0].value += " (" + config.WIDTH + " pixels)";
settings.params[1].value += " (" + config.HEIGHT + " pixels)";
}
//exif data
if (config.layer._exif != undefined) {
//show exif and general data
var exif_data = config.layer._exif;
//show general data
for (var i in exif_data.general) {
settings.params.push({title: i + ":", value: exif_data.general[i]});
}
//show exif data
var n = 0;
for (var i in exif_data.exif) {
if (i == 'undefined')
continue;
if (n == 0)
settings.params.push({title: "==== EXIF ====", value: ''});
settings.params.push({title: i + ":", value: exif_data.exif[i]});
n++;
}
}
this.POP.show(settings);
//calc colors
setTimeout(function () {
var colors = _this.unique_colors_count();
colors = _this.Helper.number_format(colors, 0);
document.getElementById('pop_data_uniquecolo').innerHTML = colors;
}, 50);
}
unique_colors_count() {
var method = 'v2'; //v1 or v2
if (config.WIDTH * config.HEIGHT > 20 * 1000 * 1000) {
return '-';
}
var canvas = this.Base_layers.convert_layer_to_canvas();
var ctx = canvas.getContext("2d");
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var imgData = img.data;
//v1 - simple, slow
if (method == 'v1') {
var colors = [];
var n = 0;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
var key = imgData[i] + "." + imgData[i + 1] + "." + imgData[i + 2];
if (colors[key] == undefined) {
colors[key] = 1;
n++;
}
}
}
//v2 - 30% faster
else if (method == 'v2') {
var buffer32 = new Uint32Array(imgData.buffer);
var len = buffer32.length;
var stats = {};
var n = 0;
for (var i = 0; i < len; i++) {
var key = "" + (buffer32[i] & 0xffffff);
if (stats[key] == undefined) {
stats[key] = 0;
n++;
}
}
}
return n;
}
}
export default Image_information_class;
@@ -0,0 +1,56 @@
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
class Image_opacity_class {
constructor() {
this.POP = new Dialog_class();
}
opacity() {
var _this = this;
var initial_opacity = config.layer.opacity;
var settings = {
title: 'Opacity',
params: [
{name: "opacity", title: "Alpha:", value: config.layer.opacity, range: [0, 100]},
],
on_change: function (params, canvas_preview, w, h) {
_this.opacity_handler(params, false);
},
on_finish: function (params) {
config.layer.opacity = initial_opacity;
_this.opacity_handler(params);
},
on_cancel: function (params) {
config.layer.opacity = initial_opacity;
config.need_render = true;
},
};
this.POP.show(settings);
}
opacity_handler(data, is_final = true) {
var value = parseInt(data.opacity);
if (value < 0)
value = 0;
if (value > 100)
value = 100;
if (is_final) {
app.State.do_action(
new app.Actions.Bundle_action('change_opacity', 'Change Opacity', [
new app.Actions.Update_layer_action(config.layer.id, {
opacity: value
})
])
);
} else {
config.layer.opacity = value;
config.need_render = true;
}
}
}
export default Image_opacity_class;
@@ -0,0 +1,53 @@
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import colorThief_class from './../../libs/color-thief.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import Dialog_class from './../../libs/popup.js';
import Helper_class from './../../libs/helpers.js';
class Image_color_class {
constructor() {
this.Base_layers = new Base_layers_class();
this.alertify = new colorThief_class();
this.POP = new Dialog_class();
this.Helper = new Helper_class();
}
palette() {
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var palette = this.alertify.getPalette(config.layer.link);
var dominant = this.alertify.getColor(config.layer.link);
dominant = this.Helper.rgbToHex(dominant[0], dominant[1], dominant[2]);
var settings = {
title: 'Palette',
params: [
{title: "Dominant color:", html: this.generate_color_box(dominant, 200)},
],
};
for (var i in palette) {
var rgb = this.Helper.rgbToHex(palette[i][0], palette[i][1], palette[i][2]);
i = parseInt(i);
settings.params.push(
{title: "Color #" + (i + 1) + ":", html: this.generate_color_box(rgb, 100)}
);
}
this.POP.show(settings);
}
generate_color_box(color, width) {
var html = '';
html += '<input style="width:100px;margin-right:10px;" type="text" value="' + color + '" />';
html += '<span style="display:inline-block;width:' + width + 'px;height:21px;margin-bottom:-6px;border:1px solid black;background-color:' + color + '"></span>';
return html;
}
}
export default Image_color_class;
@@ -0,0 +1,287 @@
/**
* Prepare for Print — one-click AI upscale + frame fit.
*
* Shows a quality assessment (current effective DPI, needed upscale factor,
* AI vs Lanczos note) then chains AI upscale → frame-fit in a single backend call.
*
* Menu target: image/print_prepare.print_prepare
*/
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';
import { getCapabilities } from './../../api/capabilities.js';
import { showProgress, updateProgress, hideProgress } from './../../libs/progress_overlay.js';
const FRAME_SIZES = [
'5x7', '8x10', '11x14', '18x24', '16x20', '20x24', '24x36',
];
// Portrait pixels at 300 DPI (label use only)
const FRAME_PX = {
'5x7': [1500, 2100], '8x10': [2400, 3000],
'11x14': [3300, 4200], '18x24': [5400, 7200],
'16x20': [4800, 6000], '20x24': [6000, 7200],
'24x36': [7200, 10800],
};
// Actual frame inches (portrait w, h)
const FRAME_IN = {
'5x7': [5, 7], '8x10': [8, 10], '11x14': [11, 14],
'18x24': [18, 24], '16x20': [16, 20], '20x24': [20, 24],
'24x36': [24, 36],
};
var instance = null;
class Image_print_prepare_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
async print_prepare() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
var caps = await getCapabilities();
var hasAI = (caps.remote && caps.remote.healthy) || (caps.local && caps.local.local_gpu_available);
var W = config.layer.width_original;
var H = config.layer.height_original;
var qualityHtml = _buildQualityHtml(W, H, hasAI);
var frameLabels = FRAME_SIZES.map(s => {
var px = FRAME_PX[s] || [0, 0];
return `${s}" (${px[0]}×${px[1]}px @ 300dpi)`;
});
var _this = this;
this.Dialog.show({
title: 'Prepare for Print',
params: [
{
title: '',
html: qualityHtml,
},
{
name: 'frame',
title: 'Target frame size:',
value: frameLabels[0],
values: frameLabels,
type: 'select',
},
{
name: 'orientation',
title: 'Orientation:',
value: 'auto',
values: ['auto', 'portrait', 'landscape'],
type: 'select',
},
{
name: 'target_dpi',
title: 'Target DPI:',
value: '300',
values: ['200', '300'],
type: 'select',
comment: '200 dpi is fine for 18×24" and larger (viewed from a distance)',
},
{
name: 'mode',
title: 'Fit mode:',
value: 'smart',
values: ['smart', 'crop', 'extend'],
type: 'select',
comment: 'smart = extend if gap <15%, else crop',
},
{
name: 'upscale_method',
title: 'Upscale engine:',
value: 'auto',
values: ['auto', 'realesrgan_pytorch', 'realesrgan_ncnn', 'lanczos'],
type: 'select',
comment: hasAI ? 'auto picks Real-ESRGAN — genuinely adds detail' : 'auto picks Real-ESRGAN if available, else Lanczos',
},
{
name: 'prompt',
title: 'Extend prompt (optional):',
value: '',
placeholder: 'e.g. "natural background continuation" — blank works well',
},
{
name: 'new_layer',
title: 'Result as new layer (keep original):',
value: true,
},
],
on_finish: async function (params) {
var frameKey = params.frame.split('"')[0];
await _this._run(frameKey, params, W, H);
},
});
}
async _run(frameKey, params, origW, origH) {
if (this.isProcessing) return;
this.isProcessing = true;
var dpi = parseInt(params.target_dpi) || 300;
var inches = FRAME_IN[frameKey] || [8, 10];
var targetW = inches[0] * dpi;
var targetH = inches[1] * dpi;
// Orientation swap for display
var orient = params.orientation || 'auto';
var imgLandscape = origW >= origH;
var frameLandscape = inches[0] >= inches[1];
if (orient === 'landscape' || (orient === 'auto' && imgLandscape && !frameLandscape)) {
targetW = Math.max(inches[0], inches[1]) * dpi;
targetH = Math.min(inches[0], inches[1]) * dpi;
} else if (orient === 'portrait' || (orient === 'auto' && !imgLandscape && frameLandscape)) {
targetW = Math.min(inches[0], inches[1]) * dpi;
targetH = Math.max(inches[0], inches[1]) * dpi;
}
var neededScale = Math.max(targetW / origW, targetH / origH);
var willUpscale = neededScale > 1.05;
showProgress(
willUpscale
? `Upscaling ${neededScale.toFixed(1)}× with AI, then fitting to frame…\nAI is reconstructing detail — this may take 13 minutes.`
: 'Fitting to frame…',
willUpscale ? 120 : 8
);
try {
var layerCanvas = document.createElement('canvas');
layerCanvas.width = origW;
layerCanvas.height = origH;
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/prepare`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageB64,
frame: frameKey,
orientation: orient,
target_dpi: dpi,
upscale_method: params.upscale_method || 'auto',
mode: params.mode || 'smart',
prompt: params.prompt || '',
}),
});
if (!r.ok) {
var err = await r.json().catch(() => ({ detail: 'Server error' }));
throw new Error(err.detail || 'Prepare failed');
}
var result = await r.json();
updateProgress(90, 'Placing result…');
var img = new Image();
img.onload = () => {
var resultCanvas = document.createElement('canvas');
resultCanvas.width = img.naturalWidth;
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
var fitW = img.naturalWidth;
var fitH = img.naturalHeight;
if (params.new_layer) {
app.State.do_action(
new app.Actions.Bundle_action('print_prepare_layer', 'Prepare for Print', [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({ WIDTH: fitW, HEIGHT: fitH }),
new app.Actions.Insert_layer_action({
name: `${frameKey} ${dpi}dpi`,
type: 'image',
data: img.src,
x: 0, y: 0,
width: fitW, height: fitH,
width_original: fitW, height_original: fitH,
}),
new app.Actions.Prepare_canvas_action('do'),
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('print_prepare', 'Prepare for Print', [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({ WIDTH: fitW, HEIGHT: fitH }),
new app.Actions.Update_layer_image_action(resultCanvas),
new app.Actions.Prepare_canvas_action('do'),
])
);
}
hideProgress();
var upscaleNote = result.upscale_applied
? ` · ${result.upscale_factor}× ${result.upscale_method}`
: ' · no upscale needed';
alertify.success(
`Print-ready! ${fitW}×${fitH}px @ ${dpi} DPI (${frameKey}")${upscaleNote}`
);
this.isProcessing = false;
};
img.onerror = () => {
hideProgress();
alertify.error('Failed to load result.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
hideProgress();
alertify.error('Prepare for Print failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
function _buildQualityHtml(W, H, hasAI) {
var rows = FRAME_SIZES.map(key => {
var inches = FRAME_IN[key];
// Effective DPI: smaller of the two dimensions (limiting factor)
var effDpi = Math.round(Math.min(W / inches[0], H / inches[1]));
var quality = effDpi >= 300 ? '✓ excellent'
: effDpi >= 200 ? '✓ good for large format'
: effDpi >= 150 ? '~ acceptable'
: '✗ needs upscaling';
var color = effDpi >= 300 ? '#44cc44'
: effDpi >= 200 ? '#88cc44'
: effDpi >= 150 ? '#ffaa44'
: '#ff6644';
var neededScale = Math.max(1, Math.ceil((300 / effDpi) * 10) / 10);
var scaleNote = effDpi >= 300 ? '' : ` → need ~${neededScale.toFixed(1)}× upscale`;
return `<tr>
<td style="color:#aaa;padding:2px 10px 2px 0;white-space:nowrap">${key}"</td>
<td style="color:#ddd;white-space:nowrap">${effDpi} DPI</td>
<td style="color:${color};padding-left:8px">${quality}${scaleNote}</td>
</tr>`;
}).join('');
var aiNote = hasAI
? '<span style="color:#44cc44">Real-ESRGAN available — will add genuine sharpness (AI reconstructs detail)</span>'
: '<span style="color:#ffaa44">No AI provider — will use Lanczos (resizes but doesn\'t add detail)</span>';
return `<div style="font-size:11px;margin:0 0 8px">
<div style="color:#aaa;margin-bottom:6px">Current image: <span style="color:#ddd">${W}×${H}px</span> · ${aiNote}</div>
<table style="width:100%;border-collapse:collapse;margin-bottom:6px">${rows}</table>
<div style="color:#888">200 DPI is fine for 18×24" and larger prints viewed from 2+ feet.</div>
</div>`;
}
export default Image_print_prepare_class;
@@ -0,0 +1,150 @@
/**
* Remove Background Module - Uses AI to remove background and create transparent 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 apiService from './../../services/api.js';
var instance = null;
class Image_remove_background_class {
constructor() {
// Singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
}
/**
* Remove background from current layer
* Creates a new layer with transparent background
*/
async remove_background() {
var _this = this;
if (this.isProcessing) {
alertify.warning('Already processing... please wait');
return;
}
// Check if current layer is an image
if (config.layer.type != 'image') {
alertify.error('Current layer must be an image');
return;
}
var settings = {
title: 'Remove Background',
params: [
{ name: "info", title: "AI will detect the main subject and remove the background.", type: "label" },
{
name: "model", title: "Model:", value: "auto", type: "select",
values: ["auto", "ben2", "birefnet-hr", "u2net"],
comment: "auto = best available (BEN2 by default). BiRefNet-HR is slower but sharper on high-res/print work.",
},
{ name: "new_layer", title: "Create as new layer:", value: true },
{ name: "trim_result", title: "Trim transparent edges:", value: false },
],
on_finish: async function (params) {
await _this.do_remove_background(params);
},
};
this.Dialog.show(settings);
}
async do_remove_background(params) {
this.isProcessing = true;
alertify.message('AI is removing background... this may take a moment');
try {
// Get current layer image as base64
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = config.layer.width_original;
canvas.height = config.layer.height_original;
ctx.drawImage(config.layer.link, 0, 0);
var imageData = canvas.toDataURL('image/png').split(',')[1];
// Call backend API
var result = await apiService.removeBackground(imageData, params.model);
// Create image from result
var resultImage = new Image();
resultImage.onload = () => {
if (params.new_layer) {
// Create as new layer
var layerParams = {
x: config.layer.x,
y: config.layer.y,
width: resultImage.width,
height: resultImage.height,
width_original: resultImage.width,
height_original: resultImage.height,
type: 'image',
name: config.layer.name + ' (No BG)',
data: 'data:image/png;base64,' + result.result
};
app.State.do_action(
new app.Actions.Bundle_action('remove_background', 'Remove Background', [
new app.Actions.Insert_layer_action(layerParams)
])
);
alertify.success('Background removed! New layer created.');
} else {
// Replace current layer
var newCanvas = document.createElement('canvas');
newCanvas.width = resultImage.width;
newCanvas.height = resultImage.height;
var newCtx = newCanvas.getContext('2d');
newCtx.drawImage(resultImage, 0, 0);
app.State.do_action(
new app.Actions.Bundle_action('remove_background', 'Remove Background', [
new app.Actions.Update_layer_image_action(newCanvas, config.layer.id)
])
);
alertify.success('Background removed!');
}
// Enable transparency if not already
if (config.TRANSPARENCY == false) {
config.TRANSPARENCY = true;
this.Base_layers.render();
alertify.message('Transparency enabled to show removed background');
}
this.isProcessing = false;
};
resultImage.onerror = () => {
alertify.error('Failed to load result image');
this.isProcessing = false;
};
resultImage.src = 'data:image/png;base64,' + result.result;
} catch (error) {
console.error('Remove background error:', error);
alertify.error('Failed to remove background: ' + error.message);
this.isProcessing = false;
}
}
}
export default Image_remove_background_class;
@@ -0,0 +1,252 @@
/**
* Replace Subject — extract the primary subject from a source photo and
* composite it onto the current layer's background.
*
* Workflow:
* 1. User selects the subject area via Smart Select (optional but recommended).
* 2. Opens this module → picks a source photo.
* 3. Backend removes the background from the source photo (rembg / AI),
* scales the extracted subject to fit the selection (or the canvas center),
* applies LAB color transfer so the lighting matches the background, and
* returns the composited image.
*/
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';
import { showProgress, hideProgress } from './../../libs/progress_overlay.js';
const BASE = window.API_BASE_URL || '';
var instance = null;
class Image_replace_subject_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.isProcessing = false;
}
replace_subject() {
if (this.isProcessing) {
alertify.warning('Already processing… please wait');
return;
}
if (config.layer.type !== 'image') {
alertify.error('Current layer must be an image.');
return;
}
this._showDialog();
}
// ── Private ───────────────────────────────────────────────────────────────
_showDialog() {
var hasSel = !!(window.smartSelectMask && window.smartSelectMask.canvas);
// Build a dialog manually so we can embed a file input
var overlay = document.createElement('div');
overlay.style.cssText = [
'position:fixed', 'inset:0', 'background:rgba(0,0,0,0.6)',
'z-index:20000', 'display:flex', 'align-items:center', 'justify-content:center',
].join(';');
var box = document.createElement('div');
box.style.cssText = [
'background:#1a1a2e', 'border:1px solid #3a3a6a', 'border-radius:12px',
'padding:24px', 'min-width:380px', 'max-width:460px',
'font-family:sans-serif', 'color:#d0d0e0', 'font-size:13px',
].join(';');
// Title
var title = document.createElement('div');
title.textContent = 'Replace Subject';
title.style.cssText = 'font-size:16px;font-weight:bold;color:#aaaaff;margin-bottom:6px';
box.appendChild(title);
var sub = document.createElement('div');
sub.textContent = hasSel
? 'Subject will be placed inside your current selection.'
: 'No selection active — subject will be centred on the canvas. Use Smart Select first for precise placement.';
sub.style.cssText = 'font-size:11px;color:#7777aa;margin-bottom:16px;line-height:1.4';
box.appendChild(sub);
// File picker label + preview row
var fileRow = document.createElement('div');
fileRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:12px';
var fileLabel = document.createElement('label');
fileLabel.textContent = 'Source photo:';
fileLabel.style.cssText = 'color:#aaa;width:100px;flex-shrink:0';
var fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.cssText = 'flex:1;background:#0f0f1a;color:#ccc;border:1px solid #4a4a8a;border-radius:5px;padding:4px 8px;font-size:12px;cursor:pointer';
fileRow.appendChild(fileLabel);
fileRow.appendChild(fileInput);
box.appendChild(fileRow);
// Thumbnail preview
var preview = document.createElement('img');
preview.style.cssText = 'display:none;max-width:100%;max-height:160px;border-radius:6px;margin-bottom:12px;border:1px solid #3a3a6a';
box.appendChild(preview);
fileInput.addEventListener('change', () => {
var f = fileInput.files[0];
if (!f) return;
var url = URL.createObjectURL(f);
preview.src = url;
preview.style.display = 'block';
preview.onload = () => URL.revokeObjectURL(url);
});
// Match colors checkbox
var colorRow = document.createElement('div');
colorRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:16px';
var colorCheck = document.createElement('input');
colorCheck.type = 'checkbox';
colorCheck.checked = true;
colorCheck.id = 'rs-match-colors';
var colorLabel = document.createElement('label');
colorLabel.htmlFor = 'rs-match-colors';
colorLabel.textContent = 'Match background lighting & color tone';
colorLabel.style.cssText = 'color:#bbb;cursor:pointer';
colorRow.appendChild(colorCheck);
colorRow.appendChild(colorLabel);
box.appendChild(colorRow);
// Buttons
var btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex;gap:8px;justify-content:flex-end';
var cancelBtn = _btn('Cancel', '#2a2a4a', '#8888aa');
cancelBtn.onclick = () => { document.body.removeChild(overlay); };
var goBtn = _btn('Replace Subject', '#1a3a5a', '#88ccff');
goBtn.style.fontWeight = 'bold';
goBtn.onclick = async () => {
var file = fileInput.files[0];
if (!file) {
alertify.warning('Please pick a source photo first.');
return;
}
document.body.removeChild(overlay);
await this._run(file, colorCheck.checked);
};
btnRow.appendChild(cancelBtn);
btnRow.appendChild(goBtn);
box.appendChild(btnRow);
overlay.appendChild(box);
overlay.addEventListener('click', (e) => { if (e.target === overlay) document.body.removeChild(overlay); });
document.body.appendChild(overlay);
}
async _run(file, matchColors) {
this.isProcessing = true;
showProgress('Extracting subject and compositing…', 20);
try {
var subjectBase64 = await _fileToBase64(file);
var bgBase64 = _getLayerBase64();
var maskBase64 = _getMaskBase64();
var res = await _post('/api/image/replace-subject', {
background_image: bgBase64,
subject_image: subjectBase64,
mask: maskBase64 || undefined,
match_colors: matchColors,
});
var img = new Image();
img.onload = () => {
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
app.State.do_action(
new app.Actions.Bundle_action('replace_subject', 'Replace Subject', [
new app.Actions.Update_layer_image_action(canvas, config.layer.id)
])
);
// Clear selection if one was used
if (window.smartSelectMask) {
window.smartSelectMask = null;
config.need_render = true;
}
hideProgress();
alertify.success('Subject replaced!');
this.isProcessing = false;
};
img.onerror = () => {
hideProgress();
alertify.error('Failed to load result image.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + res.result;
} catch (e) {
hideProgress();
alertify.error('Replace subject failed: ' + (e.message || e));
this.isProcessing = false;
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function _getLayerBase64() {
var canvas = document.createElement('canvas');
canvas.width = config.layer.width_original;
canvas.height = config.layer.height_original;
canvas.getContext('2d').drawImage(config.layer.link, 0, 0);
return canvas.toDataURL('image/png').split(',')[1];
}
function _getMaskBase64() {
var m = window.smartSelectMask;
if (!m || !m.canvas) return null;
var w = config.layer.width_original;
var h = config.layer.height_original;
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
canvas.getContext('2d').drawImage(m.canvas, 0, 0, w, h);
return canvas.toDataURL('image/png').split(',')[1];
}
function _fileToBase64(file) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = (e) => resolve(e.target.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function _btn(text, bg, color) {
var b = document.createElement('button');
b.textContent = text;
b.style.cssText = 'background:' + bg + ';color:' + color + ';border:1px solid #3a3a6a;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px';
return b;
}
async function _post(path, body) {
var r = await fetch(BASE + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!r.ok) {
var err = await r.json().catch(() => ({ detail: r.statusText }));
throw new Error(err.detail || 'Request failed');
}
return r.json();
}
export default Image_replace_subject_class;
@@ -0,0 +1,477 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import Base_gui_class from './../../core/base-gui.js';
import Dialog_class from './../../libs/popup.js';
import ImageFilters_class from './../../libs/imagefilters.js';
import Hermite_class from 'hermite-resize';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import Pica from './../../../../node_modules/pica/dist/pica.js';
import Helper_class from './../../libs/helpers.js';
import Tools_settings_class from './../tools/settings.js';
import { metaDefaults as textMetaDefaults } from '../../tools/text.js';
var instance = null;
class Image_resize_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Base_gui = new Base_gui_class();
this.POP = new Dialog_class();
this.ImageFilters = ImageFilters_class;
this.Hermite = new Hermite_class();
this.Tools_settings = new Tools_settings_class();
this.pica = Pica();
this.Helper = new Helper_class();
this._lastUnits = 'pixels';
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.keyCode;
if (this.Helper.is_input(event.target))
return;
if (code == 82 && event.ctrlKey != true && event.metaKey != true) {
//R - resize
this.resize();
event.preventDefault();
}
}, false);
}
resize() {
var _this = this;
var savedUnits = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var displayUnits = (savedUnits === 'inches') ? 'inches' : 'pixels';
this._lastUnits = displayUnits;
var width = this.Helper.get_user_unit(config.WIDTH, displayUnits, resolution);
var height = this.Helper.get_user_unit(config.HEIGHT, displayUnits, resolution);
var settings = {
title: 'Resize',
params: [
{name: "units", title: "Units:", value: displayUnits, values: ["pixels", "inches"]},
{name: "width", title: "Width:", value: '', placeholder: width, comment: displayUnits},
{name: "height", title: "Height:", value: '', placeholder: height, comment: displayUnits},
{name: "width_percent", title: "Width (%):", value: '', placeholder: 100, comment: "%"},
{name: "height_percent", title: "Height (%):", value: '', placeholder: 100, comment: "%"},
{name: "mode", title: "Mode:", values: ["Lanczos", "Hermite", "Basic"]},
{name: "crop_to_fill", title: "Crop to fill:", value: false},
{name: "sharpen", title: "Sharpen:", value: false},
{name: "layers", title: "Layers:", values: ["All", "Active"], value: "All"},
],
on_change: function(params) {
_this.units_change_handler(params);
},
on_finish: function (params) {
_this.do_resize(params);
},
};
this.POP.show(settings);
document.getElementById("pop_data_width").select();
}
/**
* Called on any dialog field change; reacts only when the units radio switches.
* Updates width/height placeholders and labels, and persists the choice globally.
*/
units_change_handler(params) {
var units = params.units;
if (units === this._lastUnits) return;
this._lastUnits = units;
var resolution = this.Tools_settings.get_setting('resolution');
// Persist so Canvas Size and other dialogs open with the same units
const unitShort = {pixels: 'px', inches: '"', centimeters: 'cm', millimetres: 'mm'};
this.Tools_settings.save_setting('default_units', units);
this.Tools_settings.save_setting('default_units_short', unitShort[units] || units);
var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution);
var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution);
var widthInput = document.getElementById('pop_data_width');
var heightInput = document.getElementById('pop_data_height');
if (widthInput) {
widthInput.placeholder = newWidth;
widthInput.value = '';
}
if (heightInput) {
heightInput.placeholder = newHeight;
heightInput.value = '';
}
var wComment = widthInput ? widthInput.nextElementSibling : null;
var hComment = heightInput ? heightInput.nextElementSibling : null;
if (wComment && wComment.classList.contains('field_comment')) wComment.textContent = units;
if (hComment && hComment.classList.contains('field_comment')) hComment.textContent = units;
}
async do_resize(params) {
//validate
if (isNaN(params.width) && isNaN(params.height) && isNaN(params.width_percent) && isNaN(params.height_percent)) {
alertify.error('Missing at least 1 size parameter.');
return false;
}
// Crop-to-fill: scale to cover then center-crop; requires both dimensions
if (params.crop_to_fill == true) {
if (isNaN(params.width) || isNaN(params.height)) {
alertify.error('Crop to fill requires both Width and Height.');
return false;
}
if (params.layers == 'All') {
return this.do_resize_crop_fill(params);
}
}
// Build a list of actions to execute for resize
let actions = [];
if (params.layers == 'All') {
//resize all layers
var skips = 0;
for (var i in config.layers) {
try {
actions = actions.concat(await this.resize_layer(config.layers[i], params));
} catch (error) {
skips++;
}
}
if (skips > 0) {
alertify.error(skips + ' layer(s) were skipped.');
}
actions = actions.concat(this.resize_gui(params));
}
else {
//only active
actions = actions.concat(await this.resize_layer(config.layer, params));
}
return app.State.do_action(
new app.Actions.Bundle_action('resize_layers', 'Resize Layers', actions)
);
}
/**
* Resize all image layers using cover-scale then center-crop so the subject
* looks the same regardless of target aspect ratio (no stretching).
*/
async do_resize_crop_fill(params) {
var units = params.units || this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var targetWidth = this.Helper.get_internal_unit(parseFloat(params.width), units, resolution);
var targetHeight = this.Helper.get_internal_unit(parseFloat(params.height), units, resolution);
targetWidth = parseInt(targetWidth);
targetHeight = parseInt(targetHeight);
if (!targetWidth || !targetHeight || targetWidth < 1 || targetHeight < 1) {
alertify.error('Invalid dimensions for crop to fill.');
return;
}
var srcWidth = config.WIDTH;
var srcHeight = config.HEIGHT;
// Cover scale: image fills target, excess is cropped from center
var scale = Math.max(targetWidth / srcWidth, targetHeight / srcHeight);
var scaledW = Math.round(srcWidth * scale);
var scaledH = Math.round(srcHeight * scale);
var cropX = Math.round((scaledW - targetWidth) / 2);
var cropY = Math.round((scaledH - targetHeight) / 2);
var mode = params.mode;
var sharpen = params.sharpen;
let actions = [];
for (var i in config.layers) {
var layer = config.layers[i];
if (layer.type !== 'image') continue;
if (layer.width == null || layer.height == null) continue;
var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false);
var newLayerW = Math.round(layer.width * scale);
var newLayerH = Math.round(layer.height * scale);
var useMode = mode;
if (useMode == "Hermite" && (newLayerW > canvas.width || newLayerH > canvas.height)) {
useMode = "Lanczos";
}
var tmp = document.createElement('canvas');
tmp.width = newLayerW;
tmp.height = newLayerH;
if (useMode == "Lanczos") {
await this.pica.resize(canvas, tmp, {alpha: true});
} else if (useMode == "Hermite") {
tmp.getContext('2d').drawImage(canvas, 0, 0);
this.Hermite.resample_single(tmp, newLayerW, newLayerH, true);
} else {
tmp.getContext('2d').drawImage(canvas, 0, 0, newLayerW, newLayerH);
}
if (sharpen == true) {
var ctx = tmp.getContext('2d');
var imageData = ctx.getImageData(0, 0, tmp.width, tmp.height);
ctx.putImageData(this.ImageFilters.Sharpen(imageData, 1), 0, 0);
}
var newX = Math.round(layer.x * scale) - cropX;
var newY = Math.round(layer.y * scale) - cropY;
actions.push(new app.Actions.Update_layer_image_action(tmp, layer.id));
actions.push(new app.Actions.Update_layer_action(layer.id, {
x: newX,
y: newY,
width: newLayerW,
height: newLayerH,
width_original: newLayerW,
height_original: newLayerH,
}));
}
// Update canvas dimensions to exact target
actions = actions.concat([
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: targetWidth,
HEIGHT: targetHeight,
}),
new app.Actions.Prepare_canvas_action('do'),
]);
return app.State.do_action(
new app.Actions.Bundle_action('resize_layers', 'Resize Layers', actions)
);
}
/**
* Generates actions that will resize layer (image, text, vector), returns a promise that rejects on failure.
*
* @param {object} layer
* @param {object} params
* @returns {Promise<object>} Returns array of actions to perform
*/
async resize_layer(layer, params) {
var units = params.units || this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var mode = params.mode;
var width = parseFloat(params.width);
var height = parseFloat(params.height);
var width_100 = parseInt(params.width_percent);
var height_100 = parseInt(params.height_percent);
var canvas_width = layer.width;
var canvas_height = layer.height;
var sharpen = params.sharpen;
var _this = this;
//convert units
if (isNaN(width) == false){
width = this.Helper.get_internal_unit(width, units, resolution);
}
if (isNaN(height) == false){
height = this.Helper.get_internal_unit(height, units, resolution);
}
//if dimension with percent provided
if (isNaN(width) && isNaN(height)) {
if (isNaN(width_100) == false) {
width = Math.round(config.WIDTH * width_100 / 100);
canvas_width = Math.round(config.WIDTH * width_100 / 100);
}
if (isNaN(height_100) == false) {
height = Math.round(config.HEIGHT * height_100 / 100);
canvas_height = Math.round(config.HEIGHT * height_100 / 100);
}
}
//if only 1 dimension was provided
if (isNaN(width) || isNaN(height)) {
var ratio = layer.width / layer.height;
var canvas_ratio = config.WIDTH / config.HEIGHT;
if (isNaN(width))
width = Math.round(height * ratio);
canvas_width = Math.round(canvas_height * canvas_ratio);
if (isNaN(height))
height = Math.round(width / ratio);
canvas_height = Math.round(canvas_width / canvas_ratio);
}
let new_x = params.layers == 'All' ? Math.round(layer.x * width / config.WIDTH) : layer.x;
let new_y = params.layers == 'All' ? Math.round(layer.y * height / config.HEIGHT) : layer.y;
let xratio = width / config.WIDTH;
let yratio = height / config.HEIGHT;
//is text
if (layer.type == 'text') {
let data = JSON.parse(JSON.stringify(layer.data));
for (let line of data) {
for (let span of line) {
span.meta.size = Math.ceil((span.meta.size || textMetaDefaults.size) * xratio);
span.meta.stroke_size = parseFloat((0.1 * Math.round((span.meta.stroke_size != null ? span.meta.stroke_size : textMetaDefaults.stroke_size) * xratio / 0.1)).toFixed(1));
span.meta.kerning = Math.ceil((span.meta.kerning || textMetaDefaults.kerning) * xratio);
}
}
// Return actions
return [
new app.Actions.Update_layer_action(layer.id, {
x: new_x,
y: new_y,
data,
width: layer.width * xratio,
height: layer.height * yratio
})
];
}
//is vector
else if (layer.is_vector == true && layer.width != null && layer.height != null) {
// Return actions
return [
new app.Actions.Update_layer_action(layer.id, {
x: new_x,
y: new_y,
width: layer.width * xratio,
height: layer.height * yratio
})
];
}
//only images supported at this point
else if (layer.type != 'image') {
//error - no support
alertify.error('Layer must be vector or image (convert it to raster).');
throw new Error('Layer is not compatible with resize');
}
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false);
var ctx = canvas.getContext("2d");
//validate
if (mode == "Hermite" && (width > canvas.width || height > canvas.height)) {
alertify.warning('Scaling up is not supported in Hermite, using Lanczos.');
mode = "Lanczos";
}
//resize
if (mode == "Lanczos") {
//Pica resize with max quality
var tmp_data = document.createElement("canvas");
tmp_data.width = width;
tmp_data.height = height;
await this.pica.resize(canvas, tmp_data, {
alpha: true,
})
.then((result) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = width;
canvas.height = height;
ctx.drawImage(tmp_data, 0, 0, width, height);
});
}
else if (mode == "Hermite") {
//Hermite resample
this.Hermite.resample_single(canvas, width, height, true);
}
else {
//simple resize
var tmp_data = document.createElement("canvas");
tmp_data.width = canvas.width;
tmp_data.height = canvas.height;
tmp_data.getContext("2d").drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = width;
canvas.height = height;
ctx.drawImage(tmp_data, 0, 0, width, height);
}
if (sharpen == true) {
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var filtered = _this.ImageFilters.Sharpen(imageData, 1); //add effect
ctx.putImageData(filtered, 0, 0);
}
// Return actions
return [
new app.Actions.Update_layer_image_action(canvas, layer.id),
new app.Actions.Update_layer_action(layer.id, {
x: new_x,
y: new_y,
width: canvas.width,
height: canvas.height,
width_original: canvas.width,
height_original: canvas.height
})
];
}
resize_gui(params) {
var units = params.units || this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var width = parseFloat(params.width);
var height = parseFloat(params.height);
var width_100 = parseInt(params.width_percent);
var height_100 = parseInt(params.height_percent);
//convert units
if (isNaN(width) == false){
width = this.Helper.get_internal_unit(width, units, resolution);
}
if (isNaN(height) == false){
height = this.Helper.get_internal_unit(height, units, resolution);
}
//if dimension with percent provided
if (isNaN(width) && isNaN(height)) {
if (isNaN(width_100) == false) {
width = Math.round(config.WIDTH * width_100 / 100);
}
if (isNaN(height_100) == false) {
height = Math.round(config.HEIGHT * height_100 / 100);
}
}
//if only 1 dimension was provided
if (isNaN(width) || isNaN(height)) {
var ratio = config.WIDTH / config.HEIGHT;
if (isNaN(width))
width = Math.round(height * ratio);
if (isNaN(height))
height = Math.round(width / ratio);
}
return [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: parseInt(width),
HEIGHT: parseInt(height)
}),
new app.Actions.Prepare_canvas_action('do')
];
}
}
export default Image_resize_class;
@@ -0,0 +1,180 @@
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import Base_layers_class from './../../core/base-layers.js';
import Base_gui_class from './../../core/base-gui.js';
import Helper_class from './../../libs/helpers.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import app from '../../app.js';
var instance = null;
class Image_rotate_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Base_gui = new Base_gui_class();
this.Helper = new Helper_class();
this.Dialog = new Dialog_class();
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.keyCode;
if (this.Helper.is_input(event.target))
return;
if (code == 76) {
//L - rotate left
this.left();
event.preventDefault();
}
}, false);
}
rotate() {
var _this = this;
if (config.layer.rotate === null) {
alertify.error('Rotate is not supported on this type of object. Convert to raster?');
return;
}
var angles = ['Custom', '0', '90', '180', '270'];
var initial_angle = config.layer.rotate;
var settings = {
title: 'Rotate',
params: [
{name: "rotate", title: "Rotate:", value: config.layer.rotate, range: [0, 360]},
{name: "right_angle", title: "Right angle:", values: angles},
],
on_change: function (params, canvas_preview, w, h) {
_this.rotate_handler(params, false);
},
on_finish: function (params) {
config.layer.rotate = initial_angle;
_this.rotate_handler(params);
},
on_cancel: function (params) {
config.layer.rotate = initial_angle;
config.need_render = true;
},
};
this.Dialog.show(settings);
}
rotate_handler(data, can_resize = true) {
var value = parseInt(data.rotate);
if (data.right_angle != 'Custom') {
value = parseInt(data.right_angle);
}
if (value < 0)
value = 360 + value;
if (value >= 360)
value = value - 360;
let new_rotate = value;
if (can_resize == true) {
app.State.do_action(
new app.Actions.Bundle_action('rotate_layer', 'Rotate Layer', [
new app.Actions.Update_layer_action(config.layer.id, {
rotate: new_rotate
}),
...this.check_sizes(new_rotate)
])
);
} else {
config.layer.rotate = new_rotate;
config.need_render = true;
}
}
left() {
let new_rotate = config.layer.rotate;
new_rotate -= 90;
if (new_rotate < 0)
new_rotate = 360 + new_rotate;
app.State.do_action(
new app.Actions.Bundle_action('rotate_layer', 'Rotate Layer', [
new app.Actions.Update_layer_action(config.layer.id, {
rotate: new_rotate
}),
...this.check_sizes(new_rotate)
])
);
}
right() {
let new_rotate = config.layer.rotate;
new_rotate += 90;
if (new_rotate >= 360)
new_rotate = new_rotate - 360;
app.State.do_action(
new app.Actions.Bundle_action('rotate_layer', 'Rotate Layer', [
new app.Actions.Update_layer_action(config.layer.id, {
rotate: new_rotate
}),
...this.check_sizes(new_rotate)
])
);
}
/**
* Makes sure image fits all after rotation
* @returns {array} actions to perform
*/
check_sizes(new_rotate) {
let actions = [];
var w = config.layer.width;
var h = config.layer.height;
var o = new_rotate * Math.PI / 180;
var new_x = w * Math.abs(Math.cos(o)) + h * Math.abs(Math.sin(o));
var new_y = w * Math.abs(Math.sin(o)) + h * Math.abs(Math.cos(o));
//round values
new_x = Math.ceil(Math.round(new_x * 1000) / 1000);
new_y = Math.ceil(Math.round(new_y * 1000) / 1000);
if (new_x > config.WIDTH || new_y > config.HEIGHT) {
var dx = 0;
var dy = 0;
let new_width = config.WIDTH;
let new_height = config.HEIGHT;
if (new_x > config.WIDTH) {
dx = Math.ceil(new_x - new_width) / 2;
new_width = new_x;
}
if (new_y > config.HEIGHT) {
dy = Math.ceil(new_y - new_height) / 2;
new_height = new_y;
}
actions.push(
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_layer_action(config.layer.id, {
x: config.layer.x + dx,
y: config.layer.y + dy
}),
new app.Actions.Update_config_action({
WIDTH: new_width,
HEIGHT: new_height
}),
new app.Actions.Prepare_canvas_action('do')
);
}
return actions;
}
}
export default Image_rotate_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,243 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_gui_class from './../../core/base-gui.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';
import Tools_settings_class from './../tools/settings.js';
import Helper_class from './../../libs/helpers.js';
import Pica from './../../../../node_modules/pica/dist/pica.js';
// Common print sizes at 300 DPI: [width_px, height_px, display_label]
const PRINT_SIZES = [
[1500, 2100, '5x7" Portrait'],
[2100, 1500, '5x7" Landscape'],
[2400, 3000, '8x10" Portrait'],
[3000, 2400, '8x10" Landscape'],
[3300, 4200, '11x14" Portrait'],
[4200, 3300, '11x14" Landscape'],
[3600, 4800, '18x24" Portrait 200dpi'],
[4800, 3600, '18x24" Landscape 200dpi'],
[5400, 7200, '18x24" Portrait 300dpi'],
[7200, 5400, '18x24" Landscape 300dpi'],
];
class Image_size_class {
constructor() {
this.Base_gui = new Base_gui_class();
this.Base_layers = new Base_layers_class();
this.POP = new Dialog_class();
this.Tools_settings = new Tools_settings_class();
this.Helper = new Helper_class();
this.pica = Pica();
this._lastUnits = 'pixels';
}
size() {
var _this = this;
var common_dimensions = this.Base_gui.common_dimensions;
var global_units = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var enable_autoresize = this.Tools_settings.get_setting('enable_autoresize');
var displayUnits = (global_units === 'inches') ? 'inches' : 'pixels';
this._lastUnits = displayUnits;
var resolutions = ['Custom'];
for (var i in common_dimensions) {
var value = common_dimensions[i];
resolutions.push(value[0] + 'x' + value[1] + ' - ' + value[2]);
}
// Print size presets — WxH format is parsed by existing resolution logic
for (var ps of PRINT_SIZES) {
resolutions.push(ps[0] + 'x' + ps[1] + ' - ' + ps[2] + ' Print');
}
var width = this.Helper.get_user_unit(config.WIDTH, displayUnits, resolution);
var height = this.Helper.get_user_unit(config.HEIGHT, displayUnits, resolution);
var settings = {
title: 'Canvas Size',
params: [
{name: "units", title: "Units:", value: displayUnits, values: ["pixels", "inches"]},
{name: "w", title: "Width:", value: width, placeholder: width, comment: displayUnits},
{name: "h", title: "Height:", value: height, placeholder: height, comment: displayUnits},
{name: "resolution", title: "Resolution:", values: resolutions},
{name: "layout", title: "Layout:", value: "Custom", values: ["Custom", "Landscape", "Portrait"]},
{name: "enable_autoresize", title: "Enable autoresize:", value: enable_autoresize},
{name: "in_proportion", title: "In proportion:", value: false},
{name: "resize_image", title: "Resize & crop image:", value: false},
],
on_change: function(params) {
_this.units_change_handler(params);
},
on_finish: function (params) {
_this.size_handler(params);
},
};
this.POP.show(settings);
}
units_change_handler(params) {
var units = params.units;
if (units === this._lastUnits) return;
this._lastUnits = units;
var resolution = this.Tools_settings.get_setting('resolution');
// Persist so Resize and other dialogs open with the same units
const unitShort = {pixels: 'px', inches: '"', centimeters: 'cm', millimetres: 'mm'};
this.Tools_settings.save_setting('default_units', units);
this.Tools_settings.save_setting('default_units_short', unitShort[units] || units);
var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution);
var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution);
var wInput = document.getElementById('pop_data_w');
var hInput = document.getElementById('pop_data_h');
if (wInput) wInput.value = newWidth;
if (hInput) hInput.value = newHeight;
// Update the unit label shown next to each field
var wComment = wInput ? wInput.nextElementSibling : null;
var hComment = hInput ? hInput.nextElementSibling : null;
if (wComment && wComment.classList.contains('field_comment')) wComment.textContent = units;
if (hComment && hComment.classList.contains('field_comment')) hComment.textContent = units;
}
async size_handler(data) {
var width = parseFloat(data.w);
var height = parseFloat(data.h);
var canvasRatio = config.WIDTH / config.HEIGHT;
var units = data.units || this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
if (width < 0) width = 1;
if (height < 0) height = 1;
this.Tools_settings.save_setting('enable_autoresize', data.enable_autoresize);
if (isNaN(width) && isNaN(height)) {
alertify.error('Wrong dimensions');
return;
}
if (isNaN(width)) width = height * canvasRatio;
if (isNaN(height)) height = width / canvasRatio;
if (data.resolution != 'Custom') {
var dim = data.resolution.split(" ");
dim = dim[0].split("x");
width = parseInt(dim[0]);
height = parseInt(dim[1]);
// Don't apply layout swap for print presets (orientation is already encoded)
if (data.layout == 'Portrait' && !data.resolution.includes('Print')) {
var tmp = width;
width = height;
height = tmp;
}
} else {
width = this.Helper.get_internal_unit(width, units, resolution);
height = this.Helper.get_internal_unit(height, units, resolution);
}
width = parseInt(width);
height = parseInt(height);
var actions = [
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: width,
HEIGHT: height
}),
];
// Proportional layer repositioning (only when not doing full resize+crop)
if (data.in_proportion == true && data.resize_image != true) {
var width_ratio = config.WIDTH / width;
var height_ratio = config.HEIGHT / height;
var maxRatio = Math.max(width_ratio, height_ratio);
for (var i in config.layers) {
var layer = config.layers[i];
if (layer.x != null && layer.y != null) {
actions.push(new app.Actions.Update_layer_action(layer.id, {
x: Math.round(layer.x / width_ratio),
y: Math.round(layer.y / height_ratio),
}));
}
if (layer.width != null && layer.height != null) {
actions.push(new app.Actions.Update_layer_action(layer.id, {
width: Math.round(layer.width / maxRatio),
height: Math.round(layer.height / maxRatio),
}));
}
}
}
// Resize & center-crop image layers to fill the new canvas
if (data.resize_image == true) {
try {
var cropActions = await this.get_resize_crop_actions(width, height);
actions = actions.concat(cropActions);
} catch (error) {
alertify.error('Could not resize image: ' + error.message);
}
}
actions.push(new app.Actions.Prepare_canvas_action('do'));
app.State.do_action(
new app.Actions.Bundle_action('set_image_size', 'Set Image Size', actions)
);
}
/**
* Generates actions to scale-and-center-crop all image layers to fill targetWidth × targetHeight.
* Uses "cover" scaling: the image is scaled so it fills the target, then cropped from the center.
*/
async get_resize_crop_actions(targetWidth, targetHeight) {
var actions = [];
var srcWidth = config.WIDTH;
var srcHeight = config.HEIGHT;
var scale = Math.max(targetWidth / srcWidth, targetHeight / srcHeight);
var scaledW = Math.round(srcWidth * scale);
var scaledH = Math.round(srcHeight * scale);
var cropX = Math.round((scaledW - targetWidth) / 2);
var cropY = Math.round((scaledH - targetHeight) / 2);
for (var i in config.layers) {
var layer = config.layers[i];
if (layer.type !== 'image') continue;
if (layer.width == null || layer.height == null) continue;
var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false);
var newLayerW = Math.round(layer.width * scale);
var newLayerH = Math.round(layer.height * scale);
var tmp = document.createElement('canvas');
tmp.width = newLayerW;
tmp.height = newLayerH;
await this.pica.resize(canvas, tmp, {alpha: true});
var newX = Math.round(layer.x * scale) - cropX;
var newY = Math.round(layer.y * scale) - cropY;
actions.push(new app.Actions.Update_layer_image_action(tmp, layer.id));
actions.push(new app.Actions.Update_layer_action(layer.id, {
x: newX,
y: newY,
width: newLayerW,
height: newLayerH,
width_original: newLayerW,
height_original: newLayerH,
}));
}
return actions;
}
}
export default Image_size_class;
@@ -0,0 +1,47 @@
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import Tools_settings_class from './../tools/settings.js';
import Helper_class from './../../libs/helpers.js';
class Image_translate_class {
constructor() {
this.POP = new Dialog_class();
this.Tools_settings = new Tools_settings_class();
this.Helper = new Helper_class();
}
translate() {
var _this = this;
var units = this.Tools_settings.get_setting('default_units');
var resolution = this.Tools_settings.get_setting('resolution');
var pos_x = this.Helper.get_user_unit(config.layer.x, units, resolution);
var pos_y = this.Helper.get_user_unit(config.layer.y, units, resolution);
var settings = {
title: 'Translate',
params: [
{name: "x", title: "X position:", value: pos_x},
{name: "y", title: "Y position:", value: pos_y},
],
on_finish: function (params) {
var pos_x = _this.Helper.get_internal_unit(params.x, units, resolution);
var pos_y = _this.Helper.get_internal_unit(params.y, units, resolution);
app.State.do_action(
new app.Actions.Bundle_action('translate_layer', 'Translate Layer', [
new app.Actions.Update_layer_action(config.layer.id, {
x: pos_x,
y: pos_y,
})
])
);
},
};
this.POP.show(settings);
}
}
export default Image_translate_class;
@@ -0,0 +1,305 @@
import app from './../../app.js';
import config from './../../config.js';
import Base_gui_class from './../../core/base-gui.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';
var instance = null;
class Image_trim_class {
constructor() {
//singleton
if (instance) {
return instance;
}
instance = this;
this.Base_layers = new Base_layers_class();
this.Base_gui = new Base_gui_class();
this.Helper = new Helper_class();
this.Dialog = new Dialog_class();
this.set_events();
}
set_events() {
document.addEventListener('keydown', (event) => {
var code = event.keyCode;
if (this.Helper.is_input(event.target))
return;
if (code == 84) {
//trim
this.trim();
event.preventDefault();
}
}, false);
}
trim() {
var _this = this;
var removeWhiteColor = false;
if(config.TRANSPARENCY == false)
removeWhiteColor = true;
var settings = {
title: 'Trim',
params: [
{name: "trim_layer", title: "Trim layer:", value: true},
{name: "trim_all", title: "Trim borders:", value: true},
{name: "power", title: "Power:", value: 0, max: 255},
{name: "remove_white", title: "Trim white color?", value: removeWhiteColor},
],
on_finish: async (params) => {
if (params.trim_layer == true) {
//first trim
let actions = [];
actions = actions.concat(this.trim_layer(config.layer.id, params.remove_white, params.power));
await app.State.do_action(
new app.Actions.Bundle_action('trim_layers', 'Trim Layers', actions)
);
}
if (params.trim_all == true) {
//second trim
let actions = [];
actions = actions.concat(_this.trim_all(params.remove_white, params.power));
app.State.do_action(
new app.Actions.Bundle_action('trim_layers', 'Trim Layers', actions)
);
}
},
};
this.Dialog.show(settings);
}
/**
* removes empty (white/transparent) area from top, right, bottom and left sides
* This affects layer data
*
* @param layer_id
* @param removeWhiteColor
* @param {int} power
*/
trim_layer(layer_id, removeWhiteColor = false, power = 0) {
var layer = this.Base_layers.get_layer(layer_id);
if (layer.type != 'image') {
alertify.error('Skip - layer must be image.');
return false;
}
var trim = this.get_trim_info(layer_id, removeWhiteColor, power);
trim = trim.relative;
//if image was stretched
var width_ratio = (layer.width / layer.width_original);
var height_ratio = (layer.height / layer.height_original);
//create smaller canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = trim.width / width_ratio;
canvas.height = trim.height / height_ratio;
//cut required part
ctx.translate(-trim.left / width_ratio, -trim.top / height_ratio);
canvas.getContext("2d").drawImage(layer.link, 0, 0);
ctx.translate(0, 0);
return [
new app.Actions.Update_layer_image_action(canvas, layer.id),
new app.Actions.Update_layer_action(layer.id, {
x: layer.x + trim.left,
y: layer.y + trim.top,
width: Math.ceil(canvas.width * width_ratio),
height: Math.ceil(canvas.height * height_ratio),
width_original: canvas.width,
height_original: canvas.height
})
];
}
/**
* change canvas size, so there is no empty (white/transparent) areas on top, right, bottom and left sides
* this affect canvas size and all layers positions
*
* @param removeWhiteColor
* @param {int} power
*/
trim_all(removeWhiteColor = false, power = 0) {
let actions = [];
var all_top = config.HEIGHT;
var all_left = config.WIDTH;
var all_bottom = config.HEIGHT;
var all_right = config.WIDTH;
if (removeWhiteColor == undefined) {
removeWhiteColor = false;
if (config.TRANSPARENCY == false) {
removeWhiteColor = true;
}
}
//collect info
for (let i = 0; i < config.layers.length; i++) {
let layer = config.layers[i];
if (layer.width == null || layer.height == null || layer.x == null || layer.y == null) {
//layer without dimensions
const trim_info = this.get_trim_info(layer.id, removeWhiteColor, power);
all_top = Math.min(all_top, trim_info.top);
all_left = Math.min(all_left, trim_info.left);
all_bottom = Math.min(all_bottom, trim_info.bottom);
all_right = Math.min(all_right, trim_info.right);
}
else{
all_top = Math.min(all_top, layer.y);
all_left = Math.min(all_left, layer.x);
all_bottom = Math.min(all_bottom, config.HEIGHT - layer.height - layer.y);
all_right = Math.min(all_right, config.WIDTH - layer.width - layer.x);
}
}
//move every layer
for (let i = 0; i < config.layers.length; i++) {
let layer = config.layers[i];
if (layer.x == null || layer.y == null || layer.type == null)
continue;
actions.push(
new app.Actions.Update_layer_action(layer.id, {
x: layer.x - all_left,
y: layer.y - all_top
})
);
}
//resize
actions.push(
new app.Actions.Prepare_canvas_action('undo'),
new app.Actions.Update_config_action({
WIDTH: Math.max(1, config.WIDTH - all_left - all_right),
HEIGHT: Math.max(1, config.HEIGHT - all_top - all_bottom)
}),
new app.Actions.Prepare_canvas_action('do')
);
return actions;
}
/**
* get painted area coords
*
* @param {int} layer_id
* @param {boolean} trim_white
* @param {int} power
* @returns {object} keys: top, left, bottom, right, width, height, relative
*/
get_trim_info(layer_id, trim_white, power) {
if (trim_white == undefined) {
trim_white = false;
if (config.TRANSPARENCY == false) {
trim_white = true;
}
}
if (power == undefined) {
power = 0;
}
var layer = this.Base_layers.get_layer(layer_id);
var canvas = this.Base_layers.convert_layer_to_canvas(layer_id, null, false);
var ctx = canvas.getContext("2d");
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var imgData = img.data;
var top = 0;
var left = 0;
var bottom = 0;
var right = 0;
//check top
main1:
for (var y = 0; y < img.height; y++) {
for (var x = 0; x < img.width; x++) {
var k = ((y * (img.width * 4)) + (x * 4));
if (imgData[k + 3] <= power)
continue; //transparent
if (trim_white == true && imgData[k] >= 255 - power && imgData[k + 1] >= 255 - power
&& imgData[k + 2] >= 255 - power)
continue; //white
break main1;
}
top++;
}
//check left
main2:
for (var x = 0; x < img.width; x++) {
for (var y = 0; y < img.height; y++) {
var k = ((y * (img.width * 4)) + (x * 4));
if (imgData[k + 3] <= power)
continue; //transparent
if (trim_white == true && imgData[k] >= 255 - power && imgData[k + 1] >= 255 - power
&& imgData[k + 2] >= 255 - power)
continue; //white
break main2;
}
left++;
}
//check bottom
main3:
for (var y = img.height - 1; y >= 0; y--) {
for (var x = img.width - 1; x >= 0; x--) {
var k = ((y * (img.width * 4)) + (x * 4));
if (imgData[k + 3] <= power)
continue; //transparent
if (trim_white == true && imgData[k] >= 255 - power && imgData[k + 1] >= 255 - power
&& imgData[k + 2] >= 255 - power)
continue; //white
break main3;
}
bottom++;
}
//check right
main4:
for (var x = img.width - 1; x >= 0; x--) {
for (var y = img.height - 1; y >= 0; y--) {
var k = ((y * (img.width * 4)) + (x * 4));
if (imgData[k + 3] <= power)
continue; //transparent
if (trim_white == true && imgData[k] >= 255 - power && imgData[k + 1] >= 255 - power
&& imgData[k + 2] >= 255 - power)
continue; //white
break main4;
}
right++;
}
var top_rel = top - layer.y;
var left_rel = left - layer.x;
var bottom_rel = bottom - (config.HEIGHT - layer.y - layer.height);
var right_rel = right - (config.WIDTH - layer.x - layer.width);
return {
top: top,
left: left,
bottom: bottom,
right: right,
width: canvas.width - left - right,
height: canvas.height - top - bottom,
relative: {
top: top_rel,
left: left_rel,
bottom: bottom_rel,
right: right_rel,
width: canvas.width - left - right,
height: canvas.height - top - bottom,
},
};
}
}
export default Image_trim_class;
@@ -0,0 +1,306 @@
/**
* Upscale — increase image resolution.
* Fetches available methods from /api/print/upscale/available on first open.
* Auto-selects the recommended method; user can override.
* If no AI upscaler is found, polls /api/print/upscale/install-status while
* the backend auto-installs Real-ESRGAN NCNN Vulkan, then refreshes and continues.
*
* Menu target: image/upscale.upscale
*/
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';
import { showProgress, updateProgress, hideProgress } from './../../libs/progress_overlay.js';
var instance = null;
const METHOD_LABELS = {
auto: 'Auto (best available)',
realesrgan_pytorch: 'Real-ESRGAN — PyTorch',
realesrgan_ncnn: 'Real-ESRGAN — NCNN Vulkan',
lanczos: 'Lanczos (fast, no AI)',
};
class Image_upscale_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
this._caps = null;
}
async upscale() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
// If a previous caps fetch showed no AI upscaler, check install progress
var caps = await this._fetchCaps();
if (!caps.realesrgan_pytorch && !caps.realesrgan_ncnn) {
await this._waitForInstall(caps);
// Re-fetch caps after install
this._caps = null;
caps = await this._fetchCaps();
}
this._showDialog(caps);
}
_showDialog(caps) {
var W = config.layer.width_original;
var H = config.layer.height_original;
var available = ['auto', ...caps.methods];
var methodValues = [...new Set(available)];
var methodLabels = methodValues.map(m => {
var label = METHOD_LABELS[m] || m;
if (m === 'auto') {
label = `Auto → ${caps.recommended_label}`;
} else if (m === caps.recommended && m !== 'auto') {
label += ' ★';
}
return label;
});
var deviceNote = '';
if (caps.realesrgan_pytorch) {
var dev = caps.realesrgan_pytorch_device;
var devLabel = dev === 'cuda' ? 'CUDA GPU'
: dev === 'mps' ? 'Apple Silicon'
: 'CPU (slow — ~13 min for large images)';
deviceNote += `PyTorch: ${devLabel}. `;
}
if (caps.realesrgan_ncnn) {
deviceNote += 'NCNN Vulkan binary found. ';
}
if (!caps.realesrgan_pytorch && !caps.realesrgan_ncnn) {
var installState = (caps.ncnn_install_status || {}).state;
if (installState === 'skipped') {
deviceNote = 'Headless server — no Vulkan GPU. Lanczos only. '
+ 'Install Real-ESRGAN PyTorch for AI quality on CPU.';
} else {
deviceNote = 'No AI upscaler available — Lanczos only.';
}
}
var _this = this;
this.Dialog.show({
title: 'Upscale Image',
params: [
{
title: '',
html: `<div style="font-size:11px;color:#888;margin:0 0 8px;">
Current: ${W}×${H}px<br>
${deviceNote}
</div>`,
},
{
name: 'scale',
title: 'Scale factor:',
value: '2×',
values: ['1.5×', '2×', '3×', '4×'],
type: 'select',
},
{
name: 'method',
title: 'Method:',
value: methodLabels[0],
values: methodLabels,
type: 'select',
},
{
name: 'new_layer',
title: 'Result as new layer (keep original):',
value: false,
},
],
on_finish: async function (params) {
var labelIdx = methodLabels.indexOf(params.method);
var methodKey = labelIdx >= 0 ? methodValues[labelIdx] : 'auto';
var scale = parseFloat(params.scale);
await _this._run(scale, methodKey, params.new_layer);
},
});
}
/**
* Poll install-status until done/failed/skipped, showing a progress bar.
* On headless machines the server sets state=skipped immediately — no wait.
*/
async _waitForInstall(caps) {
var installStatus = caps.ncnn_install_status || {};
var terminalStates = ['done', 'failed', 'skipped'];
if (terminalStates.includes(installStatus.state)) {
if (installStatus.state === 'skipped') {
// Headless — just proceed, dialog will show Lanczos or PyTorch CPU
alertify.message(installStatus.message || 'No Vulkan GPU — using CPU upscaler.', 4);
}
return;
}
return new Promise((resolve) => {
alertify.message(
`<div>Installing Real-ESRGAN AI upscaler…<br>
<progress id="esrgan-install-progress" value="0" max="100"
style="width:100%;margin-top:6px;"></progress>
<span id="esrgan-install-pct">0%</span></div>`,
0
);
var poll = setInterval(async () => {
try {
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale/install-status`);
if (!r.ok) return;
var s = await r.json();
var bar = document.getElementById('esrgan-install-progress');
var pct = document.getElementById('esrgan-install-pct');
if (bar) bar.value = s.progress || 0;
if (pct) pct.textContent = `${s.progress || 0}%`;
if (s.state === 'done') {
clearInterval(poll);
alertify.dismissAll();
alertify.success('Real-ESRGAN NCNN installed.');
resolve();
} else if (s.state === 'skipped') {
clearInterval(poll);
alertify.dismissAll();
alertify.message(s.message || 'No Vulkan GPU — using CPU upscaler.', 4);
resolve();
} else if (s.state === 'failed') {
clearInterval(poll);
alertify.dismissAll();
alertify.warning('AI upscaler install failed — using Lanczos.');
resolve();
}
} catch { /* network hiccup, keep polling */ }
}, 1500);
});
}
async _fetchCaps() {
if (this._caps) return this._caps;
try {
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale/available`);
if (r.ok) {
this._caps = await r.json();
}
} catch { /* ignore */ }
if (!this._caps) {
this._caps = {
lanczos: true,
realesrgan_pytorch: false,
realesrgan_ncnn: false,
recommended: 'lanczos',
recommended_label: 'Lanczos',
methods: ['lanczos'],
ncnn_install_status: { state: 'idle', progress: 0 },
};
}
return this._caps;
}
async _run(scale, method, newLayer) {
if (this.isProcessing) return;
this.isProcessing = true;
var caps = this._caps || {};
var methodLabel = method === 'auto'
? `Auto (${caps.recommended_label || 'best available'})`
: (METHOD_LABELS[method] || method);
var isAI = method !== 'lanczos';
showProgress(
`Upscaling ${scale}× with ${methodLabel}` +
(isAI ? '\nAI is reconstructing detail — this may take 30120 seconds.' : ''),
isAI ? 90 : 10
);
try {
var layerCanvas = document.createElement('canvas');
layerCanvas.width = config.layer.width_original;
layerCanvas.height = config.layer.height_original;
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageB64, scale, method }),
});
if (!r.ok) {
var err = await r.json().catch(() => ({ detail: 'Server error' }));
throw new Error(err.detail || 'Upscale failed');
}
var result = await r.json();
var img = new Image();
img.onload = () => {
var resultCanvas = document.createElement('canvas');
resultCanvas.width = img.naturalWidth;
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
var usedLabel = result.method.replace('realesrgan_pytorch_', 'ESRGAN/')
.replace('realesrgan_ncnn', 'ESRGAN/NCNN');
if (newLayer) {
app.State.do_action(
new app.Actions.Bundle_action('upscale_layer', 'Upscale', [
new app.Actions.Insert_layer_action({
name: `${scale}× ${usedLabel}`,
type: 'image',
data: img.src,
x: 0, y: 0,
width: img.naturalWidth,
height: img.naturalHeight,
width_original: img.naturalWidth,
height_original: img.naturalHeight,
})
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('upscale', 'Upscale', [
new app.Actions.Update_layer_image_action(resultCanvas)
])
);
}
hideProgress();
alertify.success(
`${result.output.width}×${result.output.height}px · ${usedLabel}`
);
this.isProcessing = false;
};
img.onerror = () => {
hideProgress();
alertify.error('Failed to load upscaled image.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
hideProgress();
alertify.error('Upscale failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
export default Image_upscale_class;
@@ -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;
@@ -0,0 +1,153 @@
/**
* Text Presets — insert a styled text layer with one click.
* Presets: Heading, Subheading, Body, Caption, Quote, Bold Label.
* Each preset sets font, size, weight, color, and positions on canvas center.
*
* Menu target: text/text_presets.add_preset
*/
import app from './../../app.js';
import config from './../../config.js';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
var instance = null;
const PRESETS = [
{
label: 'Heading',
sample: 'Add a heading',
family: 'Montserrat', size: 72, bold: true, italic: false,
fill_color: '#ffffff', stroke_size: 0,
},
{
label: 'Subheading',
sample: 'Add a subheading',
family: 'Montserrat', size: 44, bold: false, italic: false,
fill_color: '#e2e8f0', stroke_size: 0,
},
{
label: 'Body',
sample: 'Add body text',
family: 'Lato', size: 28, bold: false, italic: false,
fill_color: '#cbd5e1', stroke_size: 0,
},
{
label: 'Caption',
sample: 'Add a caption',
family: 'Lato', size: 20, bold: false, italic: true,
fill_color: '#94a3b8', stroke_size: 0,
},
{
label: 'Quote',
sample: '"Add a quote"',
family: 'Playfair Display', size: 36, bold: false, italic: true,
fill_color: '#f1f5f9', stroke_size: 0,
},
{
label: 'Bold Label',
sample: 'LABEL',
family: 'Oswald', size: 32, bold: true, italic: false,
fill_color: '#ffffff', stroke_size: 2, stroke_color: '#000000',
},
];
class Text_presets_class {
constructor() {
if (instance) return instance;
instance = this;
this.Dialog = new Dialog_class();
}
add_preset() {
var _this = this;
const labels = PRESETS.map(p => p.label);
this.Dialog.show({
title: 'Add Text',
params: [
{
title: '',
html: `<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:4px;">
${PRESETS.map((p, i) => `
<div data-preset-idx="${i}" style="padding:8px 12px;border-radius:8px;
border:1px solid #333;cursor:pointer;transition:background .12s;"
onmouseover="this.style.background='#2a2a2a'"
onmouseout="this.style.background='transparent'">
<span style="font-family:${p.family},sans-serif;font-size:${Math.min(p.size * 0.4, 22)}px;
font-weight:${p.bold ? 'bold' : 'normal'};
font-style:${p.italic ? 'italic' : 'normal'};
color:${p.fill_color};">${p.sample}</span>
<span style="float:right;font-size:10px;color:#555;">${p.family} · ${p.size}px</span>
</div>`).join('')}
</div>`,
},
{
name: 'custom_text',
title: 'Custom text (optional):',
value: '',
},
],
on_finish: async function (params) {
// Detect which preset was last hovered/clicked — use dialog value instead
const label = params.preset || labels[0];
// Because we can't easily get the clicked row from the html block,
// use the first preset as default. The user can also type a custom text.
// A nicer approach: wire click handlers after dialog renders.
_this._applyPreset(PRESETS[0], params.custom_text || '');
},
});
// Wire preset row clicks after the dialog is in DOM
requestAnimationFrame(() => {
document.querySelectorAll('[data-preset-idx]').forEach(el => {
el.addEventListener('click', () => {
const idx = parseInt(el.dataset.presetIdx, 10);
const customInput = document.querySelector('input[name="custom_text"]') ||
document.querySelector('#custom_text');
const text = customInput ? customInput.value.trim() : '';
_this._applyPreset(PRESETS[idx], text);
// Close dialog
const closeBtn = document.querySelector('.dialog_close') ||
document.querySelector('[data-dialog-close]');
if (closeBtn) closeBtn.click();
});
});
});
}
_applyPreset(preset, customText) {
const text = customText || preset.sample;
const cw = config.WIDTH || 800;
const ch = config.HEIGHT || 600;
// Build a text layer. miniPaint text layers use type='text' with params.
app.State.do_action(
new app.Actions.Insert_layer_action({
type: 'text',
name: preset.label,
x: Math.round(cw * 0.1),
y: Math.round(ch * 0.4),
width: Math.round(cw * 0.8),
height: preset.size + 20,
width_original: Math.round(cw * 0.8),
height_original: preset.size + 20,
params: {
text: text,
family: preset.family,
size: preset.size,
bold: preset.bold,
italic: preset.italic,
fill_color: preset.fill_color,
stroke_size: preset.stroke_size || 0,
stroke_color: preset.stroke_color || '#000000',
kerning: 0,
leading: 0,
},
})
);
alertify.success(`"${preset.label}" text added — double-click to edit.`);
}
}
export default Text_presets_class;
@@ -0,0 +1,271 @@
/**
* AI Provider Settings — configure remote AI provider in-app without editing .env manually.
* Settings are persisted to localStorage and sent to the backend config endpoint.
* Menu target: tools/ai_provider_settings.ai_provider_settings
*/
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import { getCapabilities, getGpuStatus } from './../../api/capabilities.js';
// localStorage key prefix
const LS = 'paintplus_ai_';
function ls_get(key, def = '') {
return localStorage.getItem(LS + key) ?? def;
}
function ls_set(key, val) {
localStorage.setItem(LS + key, val);
}
var instance = null;
class Tools_ai_provider_settings_class {
constructor() {
if (instance) return instance;
instance = this;
this.POP = new Dialog_class();
}
async ai_provider_settings() {
var _this = this;
// Fetch caps and GPU status in parallel
var caps = await getCapabilities();
var gpuStatus = null;
var local = caps.local || {};
if (local.local_gpu_available) {
gpuStatus = await getGpuStatus().catch(() => null);
}
var remote = caps.remote || {};
var statusHtml = remote.provider
? (remote.healthy
? '<span style="color:#44cc44">● ' + remote.provider + ' — connected</span>'
: '<span style="color:#ffaa00">● ' + remote.provider + ' — unreachable</span>')
: '<span style="color:#888">No remote provider configured</span>';
var gpuInfoHtml = gpuStatus ? _renderGpuInfo(gpuStatus) : '';
var providerValues = ['', 'openai', 'invokeai', 'comfyui', 'replicate', 'local_gpu'];
var params = [
{
title: 'Status:',
html: '<div style="margin:4px 0 8px;font-size:12px;">' + statusHtml + '</div>',
},
];
if (gpuInfoHtml) {
params.push({
title: '',
html: '<div style="margin:4px 0 8px"><div style="font-size:11px;color:#aaa;margin-bottom:3px">Detected GPU:</div>' + gpuInfoHtml + '</div>',
});
}
params.push(
{
name: 'provider',
title: 'Default provider (used unless overridden below):',
value: ls_get('provider', remote.provider || ''),
values: providerValues,
type: 'select',
},
// ── Per-operation overrides ───────────────────────────────
{
title: '',
html: '<div style="font-size:11px;color:#888;margin:2px 0 6px;">Per-operation overrides — blank = use default above</div>',
},
{
name: 'provider_inpaint',
title: 'Inpaint / Replace Selection:',
value: ls_get('provider_inpaint', remote.overrides?.inpaint || ''),
values: providerValues,
type: 'select',
},
{
name: 'provider_txt2img',
title: 'Text → Image:',
value: ls_get('provider_txt2img', remote.overrides?.txt2img || ''),
values: providerValues,
type: 'select',
},
{
name: 'provider_img2img',
title: 'Image → Image:',
value: ls_get('provider_img2img', remote.overrides?.img2img || ''),
values: providerValues,
type: 'select',
},
{
name: 'provider_outpaint',
title: 'Expand Canvas (Outpaint):',
value: ls_get('provider_outpaint', remote.overrides?.outpaint || ''),
values: providerValues,
type: 'select',
},
// ── OpenAI ────────────────────────────────────────────────
{
name: 'openai_key',
title: 'OpenAI API key:',
value: ls_get('openai_key'),
placeholder: 'sk-...',
},
{
name: 'openai_model',
title: 'OpenAI model:',
value: ls_get('openai_model', 'dall-e-3'),
values: ['dall-e-3', 'dall-e-2'],
type: 'select',
},
// ── InvokeAI ──────────────────────────────────────────────
{
name: 'invokeai_url',
title: 'InvokeAI URL:',
value: ls_get('invokeai_url'),
placeholder: 'http://192.168.1.x:9090',
},
{
name: 'invokeai_model',
title: 'InvokeAI default model:',
value: ls_get('invokeai_model', 'flux-dev'),
placeholder: 'flux-dev',
},
// ── ComfyUI ───────────────────────────────────────────────
{
name: 'comfyui_url',
title: 'ComfyUI URL:',
value: ls_get('comfyui_url'),
placeholder: 'http://192.168.1.x:8188',
},
{
name: 'comfyui_model',
title: 'ComfyUI default checkpoint:',
value: ls_get('comfyui_model', 'v1-5-pruned-emaonly.ckpt'),
placeholder: 'v1-5-pruned-emaonly.ckpt',
},
// ── Replicate ─────────────────────────────────────────────
{
name: 'replicate_key',
title: 'Replicate API key:',
value: ls_get('replicate_key'),
placeholder: 'r8_...',
}
);
this.POP.show({
title: 'AI Provider Settings',
params: params,
on_finish: async function (params) {
await _this._save(params);
},
});
}
async _save(params) {
// Persist to localStorage
ls_set('provider', params.provider || '');
ls_set('provider_inpaint', params.provider_inpaint || '');
ls_set('provider_txt2img', params.provider_txt2img || '');
ls_set('provider_img2img', params.provider_img2img || '');
ls_set('provider_outpaint', params.provider_outpaint || '');
ls_set('openai_key', params.openai_key || '');
ls_set('openai_model', params.openai_model || 'dall-e-3');
ls_set('invokeai_url', params.invokeai_url || '');
ls_set('invokeai_model', params.invokeai_model || 'flux-dev');
ls_set('comfyui_url', params.comfyui_url || '');
ls_set('comfyui_model', params.comfyui_model || 'v1-5-pruned-emaonly.ckpt');
ls_set('replicate_key', params.replicate_key || '');
// Push to backend
try {
var payload = {
ai_provider: params.provider || '',
ai_provider_inpaint: params.provider_inpaint || '',
ai_provider_txt2img: params.provider_txt2img || '',
ai_provider_img2img: params.provider_img2img || '',
ai_provider_outpaint: params.provider_outpaint || '',
openai_api_key: params.openai_key || '',
openai_model: params.openai_model || 'dall-e-3',
invokeai_url: params.invokeai_url || '',
invokeai_default_model: params.invokeai_model || 'flux-dev',
comfyui_url: params.comfyui_url || '',
comfyui_default_model: params.comfyui_model || '',
replicate_api_key: params.replicate_key || '',
};
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (r.ok) {
alertify.success('AI provider settings saved. Testing connection...');
var { refreshCapabilities } = await import('./../../api/capabilities.js');
var caps = await refreshCapabilities();
if (caps?.remote?.healthy) {
alertify.success('Connected to ' + caps.remote.provider + '!');
} else if (params.provider) {
if (params.provider === 'local_gpu') {
alertify.success('local_gpu set — restart the container with docker-compose.gpu.yml to activate.');
} else {
alertify.warning('Settings saved but provider is not reachable. Check URL/key.');
}
}
} else {
alertify.warning(
'Settings saved locally. To make them permanent, ' +
'set these values in your .env file and restart the server.'
);
}
} catch {
alertify.warning(
'Settings saved locally. Set AI_PROVIDER and related keys in .env to make permanent.'
);
}
}
}
function _renderGpuInfo(g) {
var flags = [
g.fp16 && 'fp16',
g.bf16 && 'bf16',
g.fp8 && 'fp8',
g.int8 && 'int8',
g.tensor_cores && 'tensor-cores',
g.xformers && 'xformers',
].filter(Boolean).join(' · ');
var rows = Object.entries(g.recommended || {})
.filter(([, s]) => s)
.map(function([op, s]) {
var modelName = s.model_id.split('/').pop();
return '<tr>' +
'<td style="color:#aaa;padding:2px 8px 2px 0;white-space:nowrap">' + op + '</td>' +
'<td style="color:#ddd">' + modelName + '</td>' +
'<td style="color:#888;padding-left:8px;font-size:10px">' + s.memory_opt + '</td>' +
'</tr>';
})
.join('');
var warnHtml = (g.warnings || []).length
? '<div style="color:#ffaa44;margin-top:6px;font-size:10px">' +
g.warnings.map(function(w) { return '⚠ ' + w; }).join('<br>') + '</div>'
: '';
return '<div style="background:#1a2a1a;border:1px solid #2a4a2a;border-radius:6px;padding:10px;font-size:11px;font-family:monospace">' +
'<div style="color:#44cc44;font-size:12px;margin-bottom:6px">⬛ ' + (g.device_name || 'GPU') + '</div>' +
'<div style="color:#aaa">VRAM: <span style="color:#ddd">' + g.vram_total_gb + ' GB total · ' + g.vram_free_gb + ' GB free</span></div>' +
'<div style="color:#aaa">Compute: <span style="color:#ddd">CC ' + g.compute_capability + '</span>' +
(flags ? ' <span style="color:#888">' + flags + '</span>' : '') + '</div>' +
'<div style="color:#aaa">Effective: <span style="color:#ddd">' + g.effective_vram_gb + ' GB</span>' +
' Tier: <span style="color:#44cc44">' + g.tier + '</span></div>' +
(rows ? '<div style="color:#aaa;margin-top:8px">Models selected:</div>' +
'<table style="width:100%;margin-top:3px">' + rows + '</table>' : '') +
warnHtml +
'</div>';
}
export default Tools_ai_provider_settings_class;
@@ -0,0 +1,82 @@
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';
class Tools_colorToAlpha_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
this.Helper = new Helper_class();
}
color_to_alpha() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Color to Alpha',
preview: true,
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params.color);
canvas_preview.putImageData(data, 0, 0);
},
params: [
{name: "color", title: "Color:", value: config.COLOR, type: 'color'},
],
on_finish: function (params) {
_this.apply_affect(params.color);
},
};
this.POP.show(settings);
}
apply_affect(color) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, color);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, color) {
var imgData = data.data;
var back_color = this.Helper.hexToRgb(color);
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
//calculate difference from requested color, and change alpha
var diff = Math.abs(imgData[i] - back_color.r) + Math.abs(imgData[i + 1] - back_color.g) + Math.abs(imgData[i + 2] - back_color.b) / 3;
imgData[i + 3] = Math.round(diff);
//combining 2 layers in future will change colors, so make changes to get same colors in final image
//color_result = color_1 * (alpha_1 / 255) * (1 - A2 / 255) + color_2 * (alpha_2 / 255)
//color_2 = (color_result - color_1 * (alpha_1 / 255) * (1 - A2 / 255)) / (alpha_2 / 255)
imgData[i] = Math.ceil((imgData[i] - back_color.r * (1 - imgData[i + 3] / 255)) / (imgData[i + 3] / 255));
imgData[i + 1] = Math.ceil((imgData[i + 1] - back_color.g * (1 - imgData[i + 3] / 255)) / (imgData[i + 3] / 255));
imgData[i + 2] = Math.ceil((imgData[i + 2] - back_color.b * (1 - imgData[i + 3] / 255)) / (imgData[i + 3] / 255));
}
return data;
}
}
export default Tools_colorToAlpha_class;
@@ -0,0 +1,83 @@
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 Tools_colorZoom_class {
constructor() {
this.POP = new Dialog_class();
this.Base_layers = new Base_layers_class();
}
color_zoom() {
var _this = this;
if (config.layer.type != 'image') {
alertify.error('This layer must contain an image. Please convert it to raster to apply this tool.');
return;
}
var settings = {
title: 'Color zoom',
preview: true,
params: [
{name: "zoom", title: "Zoom:", value: "2", range: [2, 20], },
{name: "center", title: "Center:", value: "128", range: [0, 255]},
],
on_change: function (params, canvas_preview, w, h) {
var img = canvas_preview.getImageData(0, 0, w, h);
var data = _this.change(img, params.zoom, params.center);
canvas_preview.putImageData(data, 0, 0);
},
on_finish: function (params) {
_this.save_zoom(params.zoom, params.center);
},
};
this.POP.show(settings);
}
save_zoom(zoom, center) {
//get canvas from layer
var canvas = this.Base_layers.convert_layer_to_canvas(null, true);
var ctx = canvas.getContext("2d");
//change data
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = this.change(img, zoom, center);
ctx.putImageData(data, 0, 0);
//save
return app.State.do_action(
new app.Actions.Update_layer_image_action(canvas)
);
}
change(data, zoom, center) {
var imgData = data.data;
var grey;
for (var i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] == 0)
continue; //transparent
grey = Math.round(0.2126 * imgData[i] + 0.7152 * imgData[i + 1] + 0.0722 * imgData[i + 2]);
for (var j = 0; j < 3; j++) {
var k = i + j;
if (grey > center)
imgData[k] += (imgData[k] - center) * zoom;
else if (grey < center)
imgData[k] -= (center - imgData[k]) * zoom;
if (imgData[k] < 0)
imgData[k] = 0;
if (imgData[k] > 255)
imgData[k] = 255;
}
}
return data;
}
}
export default Tools_colorZoom_class;

Some files were not shown because too many files have changed in this diff Show More