painterro
Version:
HTML5 image editing widget (js paint plugin)
1,515 lines (1,418 loc) • 52.7 kB
JavaScript
import isMobile from 'ismobilejs';
import '../css/styles.css';
import '../css/bar-styles.css';
import '../css/icons/ptroiconfont.css';
import PainterroSelecter from './selecter';
import WorkLog from './worklog';
import { genId, addDocumentObjectHelpers, KEYS, trim,
getScrollbarWidth, distance, logError,setPrimitiveToolValue } from './utils';
import PrimitiveTool from './primitive';
import ColorPicker, { HexToRGB, rgbToHex } from './colorPicker';
import { setDefaults, setParam } from './params';
import { tr } from './translation';
import ZoomHelper from './zoomHelper';
import TextTool from './text';
import Resizer from './resizer';
import Inserter from './inserter';
import Settings from './settings';
import ControlBuilder from './controlbuilder';
import PaintBucket from './paintBucket';
import Filters from './filters';
import CustomEvents from './customEvents';
import { set } from 'lodash';
class PainterroProc {
constructor(params) {
const element =document.querySelector(`#${params.id}`) || document.getElementById('app');
this.customEvents = new CustomEvents(element);
addDocumentObjectHelpers();
this.getElemByIdSafe = (id) => {
if (!id) {
throw new Error(`Can't get element with id=${id}, please create an issue here, we will easily fx it: https://github.com/devforth/painterro/issues/`);
}
return document.getElementById(id);
};
this.tools = [{
name: 'select',
hotkey: 's',
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.select.activate();
this.select.draw();
},
close: () => {
this.select.close();
this.toolContainer.style.cursor = 'auto';
},
eventListner: () => this.select,
}, {
name: 'crop',
hotkey: 'c',
activate: () => {
if (this.initText) this.wrapper.click();
this.select.doCrop();
this.closeActiveTool();
},
}, {
name: 'pixelize',
hotkey: 'p',
activate: () => {
if (this.initText) this.wrapper.click();
this.select.doPixelize();
this.closeActiveTool();
},
}, {
name: 'line',
hotkey: 'l',
controls: [
() => ({
type: 'color',
title: 'lineColor',
target: 'line',
titleFull: 'lineColorFull',
action: () => {
this.colorPicker.open(this.colorWidgetState.line);
},
}),
() => this.controlBuilder.buildLineWidthControl(1),
() => this.controlBuilder.buildShadowOnControl(2),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('line');
},
eventListner: () => this.primitiveTool,
}, {
name: 'arrow',
hotkey: 'a',
controls: [
() => ({
type: 'color',
title: 'lineColor',
target: 'line',
titleFull: 'lineColorFull',
action: () => {
this.colorPicker.open(this.colorWidgetState.line);
},
}),
() => this.controlBuilder.buildArrowLengthControl(1),
() => this.controlBuilder.buildShadowOnControl(2),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('arrow');
},
eventListner: () => this.primitiveTool,
}, {
name: 'rect',
controls: [
() => ({
type: 'color',
title: 'lineColor',
titleFull: 'lineColorFull',
target: 'line',
action: () => {
this.colorPicker.open(this.colorWidgetState.line);
},
}),
() => ({
type: 'color',
title: 'fillColor',
titleFull: 'fillColorFull',
target: 'fill',
action: () => {
this.colorPicker.open(this.colorWidgetState.fill);
},
}),
() => this.controlBuilder.buildLineWidthControl(2),
() => this.controlBuilder.buildShadowOnControl(3),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('rect');
},
eventListner: () => this.primitiveTool,
}, {
name:'filters',
controls: [
() => ({
type: 'dropdown',
title: 'filters',
titleFull: 'imageFilters',
action: () => {
const dropdown = this.activeTool.controls[0].id
const value = this.getElemByIdSafe(dropdown).value;
this.filters.setFilter(value);
},
getValue: () => this.filters.getFilter(),
getAvailableValues: () => this.filters.getFilters(),
}),
()=>(
{
type: 'int',
title: 'percents',
titleFull: 'percentsFull',
min: 0,
max: 100,
options: {eventOnChange: true},
action: () => {
const input = this.getElemByIdSafe(this.activeTool.controls[1].id);
const value = input.value;
this.filters.setPercents(value);
this.filters.applyFilter();
},
getValue: () => 1,
}
)
],
activate: () => {
if (this.initText) this.wrapper.click();
this.filters.saveInitImg()
this.toolContainer.style.cursor = 'crosshair';
}
},
{
name: 'ellipse',
controls: [
() => ({
type: 'color',
title: 'lineColor',
titleFull: 'lineColorFull',
target: 'line',
action: () => {
this.colorPicker.open(this.colorWidgetState.line);
},
}),
() => ({
type: 'color',
title: 'fillColor',
titleFull: 'fillColorFull',
target: 'fill',
action: () => {
this.colorPicker.open(this.colorWidgetState.fill);
},
}),
() => this.controlBuilder.buildLineWidthControl(2),
() => this.controlBuilder.buildShadowOnControl(3),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('ellipse');
},
eventListner: () => this.primitiveTool,
}, {
name: 'brush',
hotkey: 'b',
controls: [
() => ({
type: 'color',
title: 'lineColor',
target: 'line',
titleFull: 'lineColorFull',
action: () => {
this.colorPicker.open(this.colorWidgetState.line);
},
}),
() => this.controlBuilder.buildLineWidthControl(1),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('brush');
},
eventListner: () => this.primitiveTool,
}, {
name: 'eraser',
controls: [
() => this.controlBuilder.buildEraserWidthControl(0),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('eraser');
},
eventListner: () => this.primitiveTool,
}, {
name: 'text',
hotkey: 't',
controls: [
() => ({
type: 'color',
title: 'textColor',
titleFull: 'textColorFull',
target: 'line',
action: () => {
this.colorPicker.open(this.colorWidgetState.line, (c) => {
this.textTool.setFontColor(c.alphaColor);
});
},
}),
() =>this.controlBuilder.buildFontSizeControl(1),
() => ({
type: 'dropdown',
title: 'fontName',
titleFull: 'fontNameFull',
target: 'fontName',
action: () => {
const dropdown = this.getElemByIdSafe(this.activeTool.controls[2].id);
const font = dropdown.value;
this.textTool.setFont(font);
},
getValue: () => this.textTool.getFont(),
getAvailableValues: () => this.textTool.getFonts(),
}),
() => ({
type: 'bool',
title: 'fontIsBold',
titleFull: 'fontIsBoldFull',
target: 'fontIsBold',
action: () => {
const btn = this.getElemByIdSafe(this.activeTool.controls[3].id);
const state = !(btn.getAttribute('data-value') === 'true');
this.textTool.setFontIsBold(state);
setParam('defaultFontBold', state);
btn.setAttribute('data-value', state ? 'true' : 'false'); // invert
},
getValue: () => this.textTool.isBold,
}),
() => ({
type: 'bool',
title: 'fontIsItalic',
titleFull: 'fontIsItalicFull',
target: 'fontIsItalic',
action: () => {
const btn = this.getElemByIdSafe(this.activeTool.controls[4].id);
const state = !(btn.getAttribute('data-value') === 'true'); // invert
this.textTool.setFontIsItalic(state);
setParam('defaultFontItalic', state);
btn.setAttribute('data-value', state ? 'true' : 'false');
},
getValue: () => this.textTool.isItalic,
}),
() => ({
type: 'bool',
title: 'fontStrokeAndShadow',
titleFull: 'fontStrokeAndShadowFull',
target: 'fontStrokeAndShadow',
action: () => {
const btn = this.getElemByIdSafe(this.activeTool.controls[5].id);
const nextState = !(btn.getAttribute('data-value') === 'true');
this.textTool.setStrokeOn(nextState);
setParam('defaultTextStrokeAndShadow', nextState);
btn.setAttribute('data-value', nextState ? 'true' : 'false');
},
getValue: () => this.textTool.strokeOn,
}),
],
activate: () => {
if (this.initText) this.wrapper.click();
this.textTool.setFontColor(this.colorWidgetState.line.alphaColor);
// this.textTool.setStrokeColor(this.colorWidgetState.stroke.alphaColor);
this.toolContainer.style.cursor = 'crosshair';
},
close: () => {
this.textTool.close();
},
eventListner: () => this.textTool,
}, {
name: 'rotate',
hotkey: 'r',
activate: () => {
if (this.initText) {
this.wrapper.click();
}
const w = this.size.w;
const h = this.size.h;
const tmpData = this.ctx.getImageData(0, 0, this.size.w, this.size.h);
const tmpCan = this.doc.createElement('canvas');
tmpCan.width = w;
tmpCan.height = h;
tmpCan.getContext('2d').putImageData(tmpData, 0, 0);
this.resize(h, w);
this.ctx.save();
this.ctx.translate(h / 2, w / 2);
this.ctx.rotate((90 * Math.PI) / 180);
this.ctx.drawImage(tmpCan, -w / 2, -h / 2);
this.adjustSizeFull();
this.ctx.restore();
this.worklog.captureState();
this.closeActiveTool();
},
}, {
name: 'resize',
activate: () => {
if (this.initText) this.wrapper.click();
this.resizer.open();
},
close: () => {
this.resizer.close();
},
eventListner: () => this.resizer,
},
{
name: 'undo',
activate: () => {
if (this.initText) this.wrapper.click();
this.worklog.undoState();
},
eventListner: () => this.resizer,
},
{
name: 'redo',
activate: () => {
if (this.initText) this.wrapper.click();
this.worklog.redoState();
},
eventListner: () => this.resizer,
},
{
name: 'settings',
activate: () => {
if (this.initText) this.wrapper.click();
this.settings.open();
},
close: () => {
this.settings.close();
},
eventListner: () => this.settings,
},
{
name: 'zoomout',
activate: () => {
if (this.initText) this.wrapper.click();
this.zoomButtonActive = true;
const canvas = this.canvas;
const gbr = canvas.getBoundingClientRect();
const e = {
wheelDelta: -120,
clientX: gbr.right / 2,
clientY: gbr.bottom / 2,
};
this.curCord = [
(e.clientX - this.elLeft()) + this.scroller.scrollLeft,
(e.clientY - this.elTop()) + this.scroller.scrollTop,
];
const scale = this.getScale();
this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
this.zoomImage(e);
},
},
{
name: 'zoomin',
activate: () => {
if (this.initText) this.wrapper.click();
this.zoomButtonActive = true;
const canvas = this.canvas;
const gbr = canvas.getBoundingClientRect();
const e = {
wheelDelta: 120,
clientX: gbr.right / 2,
clientY: gbr.bottom / 2,
};
this.curCord = [
(e.clientX - this.elLeft()) + this.scroller.scrollLeft,
(e.clientY - this.elTop()) + this.scroller.scrollTop,
];
const scale = this.getScale();
this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
this.zoomImage(e);
},
},
{
name: 'bucket',
hotkey: 'f',
controls: [
() => ({
type: 'color',
title: 'fillColor',
target: 'fill',
titleFull: 'fillColorFull',
action: () => {
this.colorPicker.open(this.colorWidgetState.fill);
},
}),
],
activate: () => {
// this.clear();
// this.closeActiveTool();
this.toolContainer.style.cursor = 'crosshair';
this.primitiveTool.activate('bucket');
},
eventListner: () => this.paintBucket,
},
{
name: 'clear',
activate: () => {
this.clear();
this.closeActiveTool();
},
},
{
name: 'save',
right: true,
hotkey: () => this.params.saveByEnter ? 'enter' : false,
activate: () => {
if (this.initText) this.wrapper.click();
this.save();
this.closeActiveTool();
},
}, {
name: 'open',
right: true,
activate: () => {
if (this.initText) this.wrapper.click();
this.closeActiveTool();
const input = this.getElemByIdSafe(this.fileInputId);
input.onchange = (event) => {
const files = event.target.files || event.dataTransfer.files;
if (!files.length) {
return;
}
this.openFile(files[0]);
input.value = ''; // to allow reopen
};
input.click();
},
}, {
name: 'close',
hotkey: () => this.params.hideByEsc ? 'esc' : false,
right: true,
activate: () => {
if (this.initText) this.wrapper.click();
const doClose = () => {
this.closeActiveTool();
this.close();
this.hide();
};
if (this.params.onBeforeClose) {
this.params.onBeforeClose(this.hasUnsaved, doClose);
} else {
doClose();
}
},
},...params.customTools?.map((ct)=>{return {name:ct.name,activate:ct.callBack,iconUrl:ct.iconUrl}}) || []];
this.params = setDefaults(params, this.tools.map(t => t.name));
this.colorWidgetState = {
line: {
target: 'line',
palleteColor: this.params.activeColor,
alpha: this.params.activeColorAlpha,
alphaColor: this.params.activeAlphaColor,
},
fill: {
target: 'fill',
palleteColor: this.params.activeFillColor,
alpha: this.params.activeFillColorAlpha,
alphaColor: this.params.activeFillAlphaColor,
},
bg: {
target: 'bg',
palleteColor: this.params.backgroundFillColor,
alpha: this.params.backgroundFillColorAlpha,
alphaColor: this.params.backgroundFillAlphaColor,
},
// stroke: {
// target: 'stroke',
// palleteColor: this.params.textStrokeColor,
// alpha: this.params.textStrokeColorAlpha,
// alphaColor: this.params.textStrokeAlphaColor,
// },
};
this.currentBackground = this.colorWidgetState.bg.alphaColor;
this.currentBackgroundAlpha = this.colorWidgetState.bg.alpha;
this.controlBuilder = new ControlBuilder(this);
this.isMobile = isMobile.any;
this.toolByName = {};
this.toolByKeyCode = {};
this.tools.forEach((t) => {
if (t.controls) {
t.controls = t.controls.map(t => t());
}
this.toolByName[t.name] = t;
if (t.hotkey instanceof Function) {
t.hotkey = t.hotkey();
}
if (t.hotkey && !this.params.hiddenTools.includes(t.name)) {
if (!KEYS[t.hotkey]) {
throw new Error(`Key code for ${t.hotkey} not defined in KEYS`);
}
this.toolByKeyCode[KEYS[t.hotkey]] = t;
}
});
this.activeTool = undefined;
this.zoom = false;
this.ratioRelation = undefined;
this.id = this.params.id;
this.saving = false;
if (this.id === undefined) {
this.id = genId();
this.holderId = genId();
this.holderEl = document.createElement('div');
this.holderEl.id = this.holderId;
this.holderEl.className = 'ptro-holder-wrapper';
document.body.appendChild(this.holderEl);
this.holderEl.innerHTML = `<div id='${this.id}' class="ptro-holder"></div>`;
this.baseEl = this.getElemByIdSafe(this.id);
} else {
this.baseEl = this.getElemByIdSafe(this.id);
this.holderEl = null;
}
let bar = '';
let rightBar = '';
this.tools.filter(t => !this.params.hiddenTools.includes(t.name)).forEach((b) => {
const id = genId();
b.buttonId = id;
const hotkey = b.hotkey ? ` [${b.hotkey.toUpperCase()}]` : '';
const btn = b.iconUrl
? `<button type="button" aria-label=${b.name} class="ptro-icon-btn ptro-color-control" title="${b.name}" ` +
`id="${id}" >` +
`<img width="14" src="${b.iconUrl}" alt="${`${b.name}`}" /></button>`
: `<button type="button" aria-label=${b.name} class="ptro-icon-btn ptro-color-control" title="${tr(`tools.${b.name}`)}${hotkey}" ` +
`id="${id}" >` +
`<i class="ptro-icon ptro-icon-${b.name}"></i></button>`;
if (b.right) {
rightBar += btn;
} else {
bar += btn;
}
});
this.inserter = Inserter.get(this);
const cropper = '<div class="ptro-crp-el">' +
`${PainterroSelecter.code()}${TextTool.code()}</div>`;
this.loadedName = '';
this.doc = document;
this.wrapper = this.doc.createElement('div');
this.wrapper.id = `${this.id}-wrapper`;
this.wrapper.className = 'ptro-wrapper';
this.wrapper.innerHTML =
'<div class="ptro-scroller">' +
'<div class="ptro-center-table">' +
'<div class="ptro-center-tablecell">' +
`<canvas id="${this.id}-canvas"></canvas>` +
`<div class="ptro-substrate"></div>${cropper}` +
'</div>' +
'</div>' +
`</div>${
ColorPicker.html() +
ZoomHelper.html() +
Resizer.html() +
Settings.html(this) +
Filters.html(this) +
this.inserter.html()}`;
this.baseEl.appendChild(this.wrapper);
this.scroller = this.doc.querySelector(`#${this.id}-wrapper .ptro-scroller`);
this.bar = this.doc.createElement('div');
this.bar.id = `${this.id}-bar`;
this.bar.className = 'ptro-bar ptro-color-main';
this.fileInputId = genId();
this.bar.innerHTML =
`<div>${bar}` +
'<span class="ptro-tool-controls"></span>' +
'<span class="ptro-info"></span>' +
`<span class="ptro-bar-right">${rightBar}</span>` +
`<input id="${this.fileInputId}" type="file" style="display: none" value="none" accept="image/x-png,image/png,image/gif,image/jpeg" /></div>`;
if (this.isMobile) {
this.bar.style['overflow-x'] = 'auto';
}
this.baseEl.appendChild(this.bar);
const style = this.doc.createElement('style');
style.type = 'text/css';
style.innerHTML = this.params.styles;
this.baseEl.appendChild(style);
// this.baseEl.innerHTML = '<iframe class="ptro-iframe"></iframe>';
// this.iframe = this.baseEl.getElementsByTagName('iframe')[0];
// this.doc = this.iframe.contentDocument || this.iframe.contentWindow.document;
// this.doc.body.innerHTML = html;
this.saveBtn = this.baseEl.querySelector(`#${this.toolByName.save.buttonId}`);
if (this.toolByName.save.buttonId && this.saveBtn) {
this.saveBtn.setAttribute('disabled', 'true');
}
this.body = this.doc.body;
this.info = this.doc.querySelector(`#${this.id}-bar .ptro-info`);
this.canvas = this.doc.querySelector(`#${this.id}-canvas`);
this.ctx = this.canvas.getContext('2d');
this.toolControls = this.doc.querySelector(`#${this.id}-bar .ptro-tool-controls`);
this.toolContainer = this.doc.querySelector(`#${this.id}-wrapper .ptro-crp-el`);
this.substrate = this.doc.querySelector(`#${this.id}-wrapper .ptro-substrate`);
this.zoomHelper = new ZoomHelper(this);
this.zoomButtonActive = false;
this.select = new PainterroSelecter(this, (notEmpty) => {
[this.toolByName.crop, this.toolByName.pixelize].forEach((c) => {
this.setToolEnabled(c, notEmpty);
});
});
if (this.params.backplateImgUrl) {
this.tabelCell = this.canvas.parentElement;
this.tabelCell.style.backgroundImage = `url(${this.params.backplateImgUrl})`;
this.tabelCell.style.backgroundRepeat = 'no-repeat';
this.tabelCell.style.backgroundPosition = 'center center';
const img = new Image();
img.onload = () => {
this.resize(img.naturalWidth, img.naturalHeight);
this.adjustSizeFull();
this.worklog.captureState();
this.tabelCell.style.backgroundSize = `${window.getComputedStyle(this.substrate).width} ${window.getComputedStyle(this.substrate).height}`;
};
img.src = this.params.backplateImgUrl;
}
this.resizer = new Resizer(this);
this.settings = new Settings(this);
this.primitiveTool = new PrimitiveTool(this);
this.primitiveTool.setShadowOn(this.params.defaultPrimitiveShadowOn);
this.primitiveTool.setLineWidth(this.params.defaultLineWidth);
this.primitiveTool.setArrowLength(this.params.defaultArrowLength);
this.primitiveTool.setEraserWidth(this.params.defaultEraserWidth);
this.primitiveTool.setPixelSize(this.params.defaultPixelSize);
this.hasUnsaved = false;
this.worklog = new WorkLog(this, (state) => {
if (this.saveBtn && !state.initial) {
this.saveBtn.removeAttribute('disabled');
this.hasUnsaved = true;
}
this.setToolEnabled(this.toolByName.undo, !state.first);
this.setToolEnabled(this.toolByName.redo, !state.last);
if (this.params.onChange) {
this.params.onChange.call(this, {
image: this.imageSaver,
operationsDone: this.worklog.current.prevCount,
realesedMemoryOperations: this.worklog.clearedCount,
});
}
});
this.inserter.init(this);
this.paintBucket = new PaintBucket(this);
this.textTool = new TextTool(this);
this.filters = new Filters(this);
this.colorPicker = new ColorPicker(this, (widgetState) => {
this.colorWidgetState[widgetState.target] = widgetState;
this.doc.querySelector(
`#${this.id} .ptro-color-btn[data-id='${widgetState.target}']`).style['background-color'] =
widgetState.alphaColor;
const palletRGB = HexToRGB(widgetState.palleteColor);
if (palletRGB !== undefined) {
widgetState.palleteColor = rgbToHex(palletRGB.r, palletRGB.g, palletRGB.b);
if (widgetState.target === 'line') {
setParam('activeColor', widgetState.palleteColor);
setParam('activeColorAlpha', widgetState.alpha);
} else if (widgetState.target === 'fill') {
setParam('activeFillColor', widgetState.palleteColor);
setParam('activeFillColorAlpha', widgetState.alpha);
} else if (widgetState.target === 'bg') {
setParam('backgroundFillColor', widgetState.palleteColor);
setParam('backgroundFillColorAlpha', widgetState.alpha);
} else if (widgetState.target === 'stroke') {
setParam('textStrokeColor', widgetState.palleteColor);
setParam('textStrokeColorAlpha', widgetState.alpha);
}
}
});
this.defaultTool = this.toolByName[this.params.defaultTool];
this.tools.filter(t => this.params.hiddenTools.indexOf(t.name) === -1).forEach((b) => {
this.getBtnEl(b).onclick = () => {
if (b === this.defaultTool && this.activeTool === b) {
return;
}
const currentActive = this.activeTool;
this.closeActiveTool(true);
if (currentActive !== b) {
this.setActiveTool(b);
this.customEvents.dispatchEvent('changeActiveTool', b);
} else {
this.setActiveTool(this.defaultTool);
}
};
this.getBtnEl(b).ontouch = this.getBtnEl(b).onclick;
});
this.getBtnEl(this.defaultTool).click();
this.imageSaver = {
/**
* Returns image as base64 data url
* @param {string} type - type of data url, default image/png
* @param {string} quality - number from 0 to 1, works for `image/jpeg` or `image/webp`
*/
asDataURL: (type, quality) => {
let realType = type;
if (realType === undefined) {
if (this.loadedImageType) {
realType = this.loadedImageType;
} else {
realType = 'image/png';
}
}
return this.getAsUri(realType, quality);
},
asBlob: (type, quality) => {
let realType = type;
if (realType === undefined) {
if (this.loadedImageType) {
realType = this.loadedImageType;
} else {
realType = 'image/png';
}
}
const uri = this.getAsUri(realType, quality);
const byteString = atob(uri.split(',')[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i += 1) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], {
type: realType,
});
},
getOriginalMimeType: () => this.loadedImageType,
hasAlphaChannel: () => {
const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
for (let i = 3, n = data.length; i < n; i += 4) {
if (data[i] < 255) {
return true;
}
}
return false;
},
suggestedFileName: (type) => {
let realType = type;
if (realType === undefined) {
realType = 'png';
}
return `${(this.loadedName || `image-${genId()}`)}.${realType}`;
},
getWidth: () => this.size.w,
getHeight: () => this.size.h,
};
this.initEventHandlers();
this.hide();
this.zoomFactor = 1;
}
setToolEnabled(tool, state) {
if (tool.buttonId) {
const btn = this.getElemByIdSafe(tool.buttonId);
if (state) {
btn.removeAttribute('disabled');
} else {
btn.setAttribute('disabled', 'true');
}
}
}
getAsUri(type, quality) {
let realQuality = quality;
if (realQuality === undefined) {
realQuality = 0.92;
}
return this.canvas.toDataURL(type, realQuality);
}
getBtnEl(tool) {
return this.getElemByIdSafe(tool.buttonId);
}
save() {
if (this.saving) {
return this;
}
this.saving = true;
const btn = this.baseEl.querySelector(`#${this.toolByName.save.buttonId}`);
const icon = this.baseEl.querySelector(`#${this.toolByName.save.buttonId} > i`);
if (this.toolByName.save.buttonId && btn) {
btn.setAttribute('disabled', 'true');
}
this.hasUnsaved = false;
if (icon) {
icon.className = 'ptro-icon ptro-icon-loading ptro-spinning';
}
if (this.params.saveHandler !== undefined) {
this.params.saveHandler(this.imageSaver, (hide) => {
if (hide === true) {
this.hide();
}
if (icon) {
icon.className = 'ptro-icon ptro-icon-save';
}
this.saving = false;
});
} else {
logError('No saveHandler defined, please check documentation');
if (icon) {
icon.className = 'ptro-icon ptro-icon-save';
}
this.saving = false;
}
return this;
}
close() {
if (this.params.onClose !== undefined) {
this.params.onClose();
}
}
closeActiveTool(doNotSelect) {
if (this.activeTool !== undefined) {
if (this.activeTool.close !== undefined) {
this.activeTool.close();
}
this.toolControls.innerHTML = '';
const btnEl = this.getBtnEl(this.activeTool);
if (btnEl) {
btnEl.className =
this.getBtnEl(this.activeTool).className.replace(' ptro-color-active-control', '');
}
this.activeTool = undefined;
}
if (doNotSelect !== true) {
this.setActiveTool(this.defaultTool);
}
}
handleToolEvent(eventHandler, event) {
if (this.select.imagePlaced || this.select.area.activated) {
return this.select[eventHandler](event);
}
if (this.activeTool && this.activeTool.eventListner) {
const listner = this.activeTool.eventListner();
if (listner[eventHandler]) {
return listner[eventHandler](event);
}
}
return false;
}
handleClipCopyEvent(evt) {
let handled = false;
const clipFormat = 'image/png';
if (evt.keyCode === KEYS.c && (evt.ctrlKey || evt.metaKey)) {
if (!this.inserter.waitChoice && !this.select.imagePlaced && this.select.shown) {
const a = this.select.area;
const w = a.bottoml[0] - a.topl[0];
const h = a.bottoml[1] - a.topl[1];
const tmpCan = this.doc.createElement('canvas');
tmpCan.width = w;
tmpCan.height = h;
const tmpCtx = tmpCan.getContext('2d');
tmpCtx.drawImage(this.canvas, -a.topl[0], -a.topl[1]);
tmpCan.toBlob((b) => {
/* eslint no-undef: "off" */
navigator.clipboard.write([new ClipboardItem({ [clipFormat]: b })]);
}, clipFormat, 1.0);
handled = true;
} else {
this.canvas.toBlob((b) => {
/* eslint no-undef: "off" */
navigator.clipboard.write([new ClipboardItem({ [clipFormat]: b })]);
}, clipFormat, 1.0);
handled = true;
}
}
return handled;
}
zoomImage({ wheelDelta, clientX, clientY }, forceWheenDelta, forceZoomForce) {
let whD = wheelDelta;
if (forceWheenDelta !== undefined) {
whD = forceWheenDelta;
}
let minFactor = 1;
if (this.size.w > this.wrapper.documentClientWidth) {
minFactor = Math.min(minFactor, this.wrapper.documentClientWidth / this.size.w);
}
if (this.size.h > this.wrapper.documentClientHeight) {
minFactor = Math.min(minFactor, this.wrapper.documentClientHeight / this.size.h);
}
if (!this.zoom && this.zoomFactor > minFactor) {
this.zoomFactor = minFactor;
}
let zoomForce = 0.2;
if (forceZoomForce !== undefined) {
zoomForce = forceZoomForce;
}
this.zoomFactor += Math.sign(whD) * zoomForce;
if (this.zoomFactor < minFactor) {
this.zoom = false;
this.zoomFactor = minFactor;
} else {
this.zoom = true;
}
this.adjustSizeFull();
this.select.adjustPosition();
if (this.zoom) {
this.scroller.scrollLeft = (this.curCord[0] / this.getScale()) -
(clientX - this.wrapper.documentOffsetLeft);
this.scroller.scrollTop = (this.curCord[1] / this.getScale()) -
(clientY - this.wrapper.documentOffsetTop);
}
}
initEventHandlers() {
this.documentHandlers = {
mousedown: (e) => {
if (this.shown) {
if (this.worklog.empty &&
(e.target.className.indexOf('ptro-crp-el') !== -1 ||
e.target.className.indexOf('ptro-icon') !== -1 ||
e.target.className.indexOf('ptro-named-btn') !== -1)) {
this.clearBackground(); // clear initText
}
if (this.colorPicker.handleMouseDown(e) !== true) {
this.handleToolEvent('handleMouseDown', e);
}
}
},
touchstart: (e) => {
if (e.touches.length === 1) {
e.clientX = e.changedTouches[0].clientX;
e.clientY = e.changedTouches[0].clientY;
this.documentHandlers.mousedown(e);
} else if (e.touches.length === 2) {
const fingersDist = distance({
x: e.touches[0].clientX,
y: e.touches[0].clientY,
}, {
x: e.touches[1].clientX,
y: e.touches[1].clientY,
});
this.lastFingerDist = fingersDist;
}
},
touchend: (e) => {
e.clientX = e.changedTouches[0].clientX;
e.clientY = e.changedTouches[0].clientY;
this.documentHandlers.mouseup(e);
},
touchmove: (e) => {
if (e.touches.length === 1) {
e.clientX = e.touches[0].clientX;
e.clientY = e.touches[0].clientY;
this.documentHandlers.mousemove(e);
} else if (e.touches.length === 2) {
const fingersDist = distance({
x: e.touches[0].clientX,
y: e.touches[0].clientY,
}, {
x: e.touches[1].clientX,
y: e.touches[1].clientY,
});
const center = {
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
};
e.clientX = center.x;
e.clientY = center.y;
const fingerDistDiff = Math.abs(fingersDist - this.lastFingerDist);
const zoomForce = (fingersDist > this.lastFingerDist ? 1 : -1);
this.documentHandlers.wheel(e, zoomForce, true, fingerDistDiff * 0.001);
this.lastFingerDist = fingersDist;
e.stopPropagation();
if (!this.zoomButtonActive) e.preventDefault();
}
},
mousemove: (e) => {
const isEvenFromPtro = e.target.classList[0] === 'ptro-crp-el' || e.target.classList[0] === 'ptro-bar';
if (this.shown) {
this.handleToolEvent('handleMouseMove', e);
this.colorPicker.handleMouseMove(e);
this.zoomHelper.handleMouseMove(e);
this.curCord = [
(e.clientX - this.elLeft()) + this.scroller.scrollLeft,
(e.clientY - this.elTop()) + this.scroller.scrollTop,
];
const scale = this.getScale();
this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
if (typeof e.target.tagName !== "undefined" && e.target.tagName.toLowerCase() !== 'input'
&& e.target.tagName.toLowerCase() !== 'button' && e.target.tagName.toLowerCase() !== 'i'
&& e.target.tagName.toLowerCase() !== 'select') {
// prevent default only if we are in paintero area
if (!this.zoomButtonActive && isEvenFromPtro) e.preventDefault();
}
}
},
mouseup: (e) => {
if (this.shown) {
this.handleToolEvent('handleMouseUp', e);
this.colorPicker.handleMouseUp(e);
}
},
wheel: (e, forceWheenDelta, forceCtrlKey, zoomForce) => {
if (this.shown && !this.params.disableWheelZoom) {
if (forceCtrlKey !== undefined ? forceCtrlKey : e.ctrlKey) {
this.zoomImage(e, forceWheenDelta, zoomForce);
e.preventDefault();
}
}
},
keydown: (e) => {
const argetEl = event.target;
const ignoreForSelectors = ['input', 'textarea', 'div[contenteditable]'];
if (ignoreForSelectors.some(selector => argetEl.matches(selector))) {
return; // ignore all elemetents which could be focused
}
if (this.shown) {
if (this.colorPicker.handleKeyDown(e)) {
return;
}
if (this.handleClipCopyEvent(e)) {
return;
}
const evt = window.event ? event : e;
if (this.handleToolEvent('handleKeyDown', evt)) {
return;
}
if (
(evt.keyCode === KEYS.y && evt.ctrlKey) ||
(evt.keyCode === KEYS.z && evt.ctrlKey && evt.shiftKey)) {
this.worklog.redoState();
e.preventDefault();
if (this.params.userRedo) {
this.params.userRedo.call();
}
} else if (evt.keyCode === KEYS.z && evt.ctrlKey) {
this.worklog.undoState();
e.preventDefault();
if (this.params.userUndo) {
this.params.userUndo.call();
}
}
if (this.toolByKeyCode[event.keyCode]) {
this.getBtnEl(this.toolByKeyCode[event.keyCode]).click();
e.stopPropagation();
e.preventDefault();
}
if (this.saveBtn) {
if (evt.keyCode === KEYS.s && evt.ctrlKey) {
if (this.initText) this.wrapper.click();
this.save();
evt.preventDefault();
}
}
}
},
paste: (event) => {
if (this.initText) this.wrapper.click();
if (this.shown) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
Object.keys(items).forEach((k) => {
const item = items[k];
if (item.kind === 'file' && item.type.split('/')[0] === 'image') {
this.openFile(item.getAsFile());
event.preventDefault();
event.stopPropagation();
}
});
}
},
dragover: (event) => {
if (this.shown) {
const mainClass = event.target.classList[0];
if (mainClass === 'ptro-crp-el' || mainClass === 'ptro-bar') {
this.bar.className = 'ptro-bar ptro-color-main ptro-bar-dragover';
}
event.preventDefault();
}
},
dragleave: () => {
if (this.shown) {
this.bar.className = 'ptro-bar ptro-color-main';
}
},
drop: (event) => {
if (this.shown) {
this.bar.className = 'ptro-bar ptro-color-main';
event.preventDefault();
const file = event.dataTransfer.files[0];
if (file) {
this.openFile(file);
} else {
const text = event.dataTransfer.getData('text/html');
const srcRe = /src.*?=['"](.+?)['"]/;
const srcMatch = srcRe.exec(text);
this.inserter.handleOpen(srcMatch[1]);
}
}
},
};
this.windowHandlers = {
resize: () => {
if (this.shown) {
this.adjustSizeFull();
this.syncToolElement();
}
},
};
this.listenersInstalled = false;
}
attachEventHandlers() {
if (this.listenersInstalled) {
return;
}
// passive: false fixes Unable to preventDefault inside passive event
// listener due to target being treated as passive
Object.keys(this.documentHandlers).forEach((key) => {
this.doc.addEventListener(key, this.documentHandlers[key], { passive: false });
});
Object.keys(this.windowHandlers).forEach((key) => {
window.addEventListener(key, this.windowHandlers[key], { passive: false });
});
this.listenersInstalled = true;
}
removeEventHandlers() {
if (!this.listenersInstalled) {
return;
}
Object.keys(this.documentHandlers).forEach((key) => {
this.doc.removeEventListener(key, this.documentHandlers[key]);
});
Object.keys(this.windowHandlers).forEach((key) => {
window.removeEventListener(key, this.windowHandlers[key]);
});
this.listenersInstalled = false;
}
elLeft() {
return this.toolContainer.documentOffsetLeft + this.scroller.scrollLeft;
}
elTop() {
return this.toolContainer.documentOffsetTop + this.scroller.scrollTop;
}
fitImage(img, mimetype) {
this.loadedImageType = mimetype;
this.resize(img.naturalWidth, img.naturalHeight);
this.ctx.drawImage(img, 0, 0);
const minValue = Math.min(this.wrapper.documentClientHeight / this.size.h, this.wrapper.documentClientWidth / this.size.w);
this.zoomFactor = minValue;
this.adjustSizeFull();
this.worklog.captureState();
}
loadImage(source, mimetype) {
this.inserter.handleOpen(source, mimetype);
}
show(openImage, originalMime) {
this.shown = true;
this.scrollWidth = getScrollbarWidth();
if (this.isMobile) {
this.origOverflowY = this.body.style['overflow-y'];
if (this.params.fixMobilePageReloader) {
this.body.style['overflow-y'] = 'hidden';
}
}
this.baseEl.removeAttribute('hidden');
if (this.holderEl) {
this.holderEl.removeAttribute('hidden');
}
if (typeof openImage === 'string') {
this.loadedName = trim(
(openImage.substring(openImage.lastIndexOf('/') + 1) || '').replace(/\..+$/, ''));
this.loadImage(openImage, originalMime);
} else if (openImage !== false) {
this.clear();
}
this.attachEventHandlers();
return this;
}
hide() {
if (this.isMobile) {
this.body.style['overflow-y'] = this.origOverflowY;
}
this.shown = false;
this.baseEl.setAttribute('hidden', '');
if (this.holderEl) {
this.holderEl.setAttribute('hidden', '');
}
this.removeEventHandlers();
if (this.params.onHide !== undefined) {
this.params.onHide();
}
return this;
}
setZoom(zoomPercentage) {
if (!this.size) {
return;
}
this.zoomFactor = zoomPercentage / 100;
let minFactor = 1;
if (this.size.w > this.wrapper.documentClientWidth) {
minFactor = Math.min(minFactor, this.wrapper.documentClientWidth / this.size.w);
}
if (this.size.h > this.wrapper.documentClientHeight) {
minFactor = Math.min(minFactor, this.wrapper.documentClientHeight / this.size.h);
}
if (!this.zoom && this.zoomFactor > minFactor) {
this.zoomFactor = minFactor;
}
if (this.zoomFactor < minFactor) {
this.zoom = false;
this.zoomFactor = minFactor;
} else {
this.zoom = true;
}
this.adjustSizeFull();
this.select.adjustPosition();
const canvas = this.canvas;
const gbr = canvas.getBoundingClientRect();
this.curCord = [
(gbr.right / 2 - this.elLeft()) + this.scroller.scrollLeft,
(gbr.bottom / 2 - this.elTop()) + this.scroller.scrollTop,
];
const scale = this.getScale();
this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
if (this.zoom) {
this.scroller.scrollLeft = (this.curCord[0] / this.getScale()) -
(gbr.right / 2 - this.wrapper.documentOffsetLeft);
this.scroller.scrollTop = (this.curCord[1] / this.getScale()) -
(gbr.bottom / 2 - this.wrapper.documentOffsetTop);
}
}
doScale({width, height, scale}) {
if (scale) {
if (width || height) {
throw new Error(`You can't pass width or height and scale at the same time`)
}
this.resizer.newW = Math.round(this.size.w * scale);
this.resizer.newH = Math.round(this.size.h * scale);
} else {
if (scale) {
throw new Error(`You can't pass width or height and scale at the same time`)
}
this.resizer.newW = width || Math.round(this.size.w * (height / this.size.h));
this.resizer.newH = height || Math.round(this.size.h * (width / this.size.w));
}
this.resizer.scaleButton.onclick();
}
openFile(f) {
if (!f) {
return;
}
this.loadedName = trim((f.name || '').replace(/\..+$/, ''));
const dataUrl = (window.URL ? window.URL : window.webkitURL).createObjectURL(f);
this.loadImage(dataUrl, f.type);
}
getScale() {
return this.canvas.getAttribute('width') / this.canvas.offsetWidth;
}
adjustSizeFull() {
const ratio = this.wrapper.documentClientWidth / this.wrapper.documentClientHeight;
if (this.zoom === false && this.textTool.active === false) {
if (this.size.w > this.wrapper.documentClientWidth ||
this.size.h > this.wrapper.documentClientHeight) {
const newRelation = ratio < this.size.ratio;
this.ratioRelation = newRelation;
if (newRelation) {
this.canvas.style.width = `${this.wrapper.clientWidth}px`;
this.canvas.style.height = 'auto';
} else {
this.canvas.style.width = 'auto';
this.canvas.style.height = `${this.wrapper.clientHeight}px`;
}
} else {
this.canvas.style.width = 'auto';
this.canvas.style.height = 'auto';
this.ratioRelation = 0;
}
this.scroller.style.overflow = 'hidden';
} else {
if (this.zoom === false) {
this.scroller.style.overflow = 'hidden';
} else {
this.scroller.style.overflow = 'scroll';
}
this.canvas.style.width = `${this.size.w * this.zoomFactor}px`;
this.canvas.style.height = `${this.size.h * this.zoomFactor}px`;
this.ratioRelation = 0;
}
this.syncToolElement();
this.select.draw();
}
resize(x, y) {
this.info.innerHTML = `${x}<span>x</span>${y}<br>${(this.originalMime || 'png').replace('image/', '')}`;
this.size = {
w: x,
h: y,
ratio: x / y,
};
this.canvas.setAttribute('width', this.size.w);
this.canvas.setAttribute('height', this.size.h);
}
syncToolElement() {
const w = Math.round(this.canvas.documentClientWidth);
const l = this.canvas.offsetLeft;
const h = Math.round(this.canvas.documentClientHeight);
const t = this.canvas.offsetTop;
this.toolContainer.style.left = `${l}px`;
this.toolContainer.style.width = `${w}px`;
this.toolContainer.style.top = `${t}px`;
this.toolContainer.style.height = `${h}px`;
this.substrate.style.left = `${l}px`;
this.substrate.style.width = `${w}px`;
this.substrate.style.top = `${t}px`;
this.substrate.style.height = `${h}px`;
}
clear() {
const w = this.params.defaultSize.width === 'fill' ? this.wrapper.clientWidth : this.params.defaultSize.width;
const h = this.params.defaultSize.height === 'fill' ? this.wrapper.clientHeight : this.params.defaultSize.height;
this.resize(w, h);
this.clearBackground();
this.worklog.captureState(true);
this.worklog.clean = true;
this.syncToolElement();
this.adjustSizeFull();
if (this.params.initText && this.worklog.empty) {
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = '#fff';
const initTexts = this.wrapper.querySelectorAll('.init-text');
if (initTexts.length > 0) {
initTexts.forEach((text) => {
text.remove();
});
}
this.initText = document.createElement('div');
this.initText.classList.add('init-text');
this.wrapper.append(this.initText);
this.initText.innerHTML = '<div style="pointer-events: none;position:absolute;top:50%;width:100%;left: 50%; transform: translate(-50%, -50%)">' +
`${this.params.initText}</div>`;
this.initText.style.left = '0';
this.initText.style.top = '0';
this.initText.style.right = '0';
this.initText.style.bottom = '0';
this.initText.style.pointerEvents = 'none';
this.initText.style['text-align'] = 'center';
this.initText.style.position = 'absolute';
this.initText.style.color = this.params.initTextColor;
this.initText.style['font-family'] = this.params.initTextStyle.split(/ (.+)/)[1];
this.initText.style['font-size'] = this.params.initTextStyle.split(/ (.+)/)[0];
this.wrapper.addEventListener('click', () => {
this.initText.remove();
this.initText = null;
}, { once: true });
}
}
clearBackground() {
this.ctx.beginPath();
this.ctx.clearRect(0, 0, this.size.w, this.size.h);
this.ctx.rect(0, 0, this.size.w, this.size.h);
this.ctx.fillStyle = this.currentBackground;
this.ctx.fill();
}
setColor(options){
this.doc.querySelector(
`#${this.id} .ptro-color-btn[data-id='${options[0]}']`).style['background-color'] =
options[1].alphaColor;
this.colorWidgetState = {...this.colorWidgetState, line: {
target: options[0],
palleteColor: options[1].palleteColor,
alpha:options[1].alpha,
alphaColor: options[1].alphaColor,
}, }
}
setLineWidth(width) {
setPrimitiveToolValue(width,this.primitiveTool,'setLineWidth','lineWidth');
}
setArrowLength(length) {
setPrimitiveToolValue(length,this.primitiveTool,'setArrowLength','arrowLength');
}
setEraserWidth(width) {
setPrimitiveToolValue(width,this.primitiveTool,'setEraserWidth','eraserWidth');
}
setPixelSize(size) {
setPrimitiveToolValue(size,this.primitiveTool,'setPixelSize','pixelSize');
}
setShadowOn(state) {
setPrimitiveToolValue(state,this.primitiveTool,'setShadowOn','shadowOn');
}
setActiveTool(b) {
this.activeTool = b;
this.zoomButtonActive = false;
const btnEl = this.getBtnEl(this.activeTool);