paintplus: vendor the app source and rename from EditmaskwithAI
Bring the full EditmaskwithAI application into the repo under paintplus/ (429 files) so the service is self-contained — the installer copies the vendored source to ~/docker/paintplus/src instead of cloning at runtime. Rename to PaintPlus (service + branding; app logic untouched): - services/editmaskwithai.sh -> services/paintplus.sh (register_service paintplus, install_paintplus, ~/docker/paintplus, Caddy paintplus:8000, Authelia option preserved) - container names -> paintplus across docker-compose*.yml; dev network -> paintplus-network - browser <title> -> "PaintPlus - AI Image Editor"; README heading -> PaintPlus with upstream provenance note - README utilities table: editmaskwithai -> paintplus Backend/frontend code (help strings referencing the old container name, the ai_photo_edit.db filename) is intentionally left as-is to avoid touching application logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
This commit is contained in:
@@ -0,0 +1,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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
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 {
|
||||
//Floyd–Steinberg 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 ? '30–90 seconds on local GPU' : '1–3 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 / ⌘ + 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 1–3 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 — ~1–3 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 30–120 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
Reference in New Issue
Block a user