painterro
Version:
HTML5 image editing widget (js paint plugin)
583 lines (535 loc) • 18 kB
JavaScript
import { clearSelection, KEYS } from './utils';
export default class PainterroSelecter {
constructor(main, selectionCallback) {
this.main = main;
this.canvas = main.canvas;
this.wrapper = main.wrapper;
this.ctx = main.ctx;
this.areaionCallback = selectionCallback;
this.shown = false;
this.area = {
el: main.toolContainer,
rect: document.querySelector(`#${main.id} .ptro-crp-rect`),
};
this.imagePlaced = false;
this.areaionCallback(false);
}
static code() {
return '<div class="ptro-crp-rect" hidden>' +
'<div class="ptro-crp-l select-handler"></div><div class="ptro-crp-r select-handler"></div>' +
'<div class="ptro-crp-t select-handler"></div><div class="ptro-crp-b select-handler"></div>' +
'<div class="ptro-crp-tl select-handler"></div><div class="ptro-crp-tr select-handler"></div>' +
'<div class="ptro-crp-bl select-handler"></div><div class="ptro-crp-br select-handler"></div>' +
'</div>';
}
activate() {
this.area.activated = true;
this.areaionCallback(false);
}
doCrop() {
const imgData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
this.main.resize(
this.area.bottoml[0] - this.area.topl[0],
this.area.bottoml[1] - this.area.topl[1]);
this.main.ctx.putImageData(imgData, -this.area.topl[0], -this.area.topl[1]);
this.main.adjustSizeFull();
this.main.worklog.captureState();
}
doPixelize() {
const c = this.area.topl;
const size = [
this.area.bottoml[0] - c[0], // width
this.area.bottoml[1] - c[1],
];
this.pixelizePixelSize = this.main.params.pixelizePixelSize;
if (this.pixelizePixelSize.slice(-1) === '%') {
this.pixelSize = (Math.min(size[0], size[1]) / (100.0 / this.pixelizePixelSize.slice(0, -1)));
} else if (this.pixelizePixelSize.slice(-2).toLowerCase() === 'px') {
this.pixelSize = this.pixelizePixelSize.slice(0, -2);
} else {
this.pixelSize = this.pixelizePixelSize;
}
if (this.pixelSize < 2) {
this.pixelSize = 2; // prevent errors
}
if (size[1] < size[0]) {
this.pixelSizeY = this.pixelSize;
const desiredHorPxs = Math.round(size[0] / this.pixelSizeY);
this.pixelSizeX = (size[0] * 1.0) / desiredHorPxs;
} else {
this.pixelSizeX = this.pixelSize;
const desiredVerPxs = Math.round(size[1] / this.pixelSizeX);
this.pixelSizeY = (size[1] * 1.0) / desiredVerPxs;
}
const pxData = [];
const pxSize = [size[0] / this.pixelSizeX, size[1] / this.pixelSizeY];
for (let i = 0; i < pxSize[0]; i += 1) {
const row = [];
for (let j = 0; j < pxSize[1]; j += 1) {
row.push([0, 0, 0, 0, 0]);
}
pxData.push(row);
}
const data = this.ctx.getImageData(c[0], c[1], size[0], size[1]);
for (let i = 0; i < size[0]; i += 1) {
for (let j = 0; j < size[1]; j += 1) {
const ii = Math.floor(i / this.pixelSizeX);
const jj = Math.floor(j / this.pixelSizeY);
const base = ((j * size[0]) + i) * 4;
pxData[ii][jj][0] += data.data[base];
pxData[ii][jj][1] += data.data[base + 1];
pxData[ii][jj][2] += data.data[base + 2];
pxData[ii][jj][3] += data.data[base + 3];
pxData[ii][jj][4] += 1;
}
}
for (let i = 0; i < pxSize[0]; i += 1) {
for (let j = 0; j < pxSize[1]; j += 1) {
const s = pxData[i][j][4];
this.ctx.fillStyle = `rgba(
${Math.round(pxData[i][j][0] / s)},
${Math.round(pxData[i][j][1] / s)},
${Math.round(pxData[i][j][2] / s)},
${Math.round(pxData[i][j][3] / s)})`;
const baseX = c[0] + (i * this.pixelSizeX);
const baseY = c[1] + (j * this.pixelSizeY);
this.ctx.fillRect(baseX, baseY, this.pixelSizeX, this.pixelSizeY);
}
}
this.main.worklog.captureState();
}
doClearArea() {
this.ctx.beginPath();
this.ctx.clearRect(
this.area.topl[0], this.area.topl[1],
this.area.bottoml[0] - this.area.topl[0], this.area.bottoml[1] - this.area.topl[1]);
this.ctx.rect(this.area.topl[0], this.area.topl[1],
this.area.bottoml[0] - this.area.topl[0], this.area.bottoml[1] - this.area.topl[1]);
this.ctx.fillStyle = this.main.currentBackground;
this.ctx.fill();
this.main.worklog.captureState();
}
selectAll() {
this.setLeft(0);
this.setRight(0);
this.setBottom(0);
this.setTop(0);
this.show();
this.reCalcCropperCords();
if (this.area.activated) {
this.areaionCallback(!this.imagePlaced &&
this.area.rect.clientWidth > 0 &&
this.area.rect.clientHeight > 0);
}
}
getScale() {
return this.canvas.clientWidth / this.canvas.getAttribute('width');
}
reCalcCropperCords() {
const ratio = this.getScale();
this.area.topl = [
Math.round(((this.rectLeft() - this.main.elLeft())) / ratio),
Math.round(((this.rectTop() - this.main.elTop())) / ratio)];
this.area.bottoml = [
Math.round(this.area.topl[0] + ((this.area.rect.clientWidth + 2) / ratio)),
Math.round(this.area.topl[1] + ((this.area.rect.clientHeight + 2) / ratio))];
}
adjustPosition() {
if (!this.shown) {
return;
}
const ratio = this.getScale();
this.setLeft(this.area.topl[0] * ratio);
this.setTop(this.area.topl[1] * ratio);
this.setRight(0);
this.setRight(this.canvas.clientWidth - (this.area.bottoml[0] * ratio));
this.setBottom(this.canvas.clientHeight - (this.area.bottoml[1] * ratio));
}
placeAt(l, t, r, b, img) {
if (this.imagePlaced) {
// for case when user inserts multiple images one after another without finishing placing them
this.finishPlacing();
}
this.main.closeActiveTool(true);
this.main.setActiveTool(this.main.defaultTool);
const scale = this.getScale();
this.setLeft(l * scale);
this.setTop(t * scale);
this.setRight(r * scale);
this.setBottom(b * scale);
const tmpCan = document.createElement('canvas');
tmpCan.width = img.naturalWidth;
tmpCan.height = img.naturalHeight;
const tmpCtx = tmpCan.getContext('2d');
tmpCtx.drawImage(img, 0, 0);
this.placedData = tmpCan.toDataURL('image/png');
const lowScale = 1000 / Math.max(img.naturalWidth, img.naturalHeight);
if (lowScale >= 1) {
this.placedDataLow = this.placedData;
} else {
tmpCan.width = img.naturalWidth * lowScale;
tmpCan.height = img.naturalHeight * lowScale;
tmpCtx.scale(lowScale, lowScale);
tmpCtx.drawImage(img, 0, 0);
this.placedDataLow = tmpCan.toDataURL('image/png');
}
this.main.select.area.rect.style['background-image'] = `url(${this.placedData})`;
this.show();
this.reCalcCropperCords();
this.imagePlaced = true;
this.main.select.activate();
this.placedRatio = img.naturalWidth / img.naturalHeight;
}
finishPlacing() {
this.imagePlaced = false;
this.main.select.area.rect.style['background-image'] = 'none';
this.main.inserter.insert(
this.area.topl[0],
this.area.topl[1],
this.area.bottoml[0] - this.area.topl[0],
this.area.bottoml[1] - this.area.topl[1]);
this.area.activated = false;
}
cancelPlacing() {
this.imagePlaced = false;
this.main.select.area.rect.style['background-image'] = 'none';
this.hide();
this.main.worklog.undoState();
}
handleKeyDown(evt) {
if (this.main.inserter.handleKeyDown(evt)) {
return true;
}
if (this.shown && this.imagePlaced) {
if (evt.keyCode === KEYS.enter) {
this.finishPlacing();
return true;
} else if (evt.keyCode === KEYS.esc) {
this.cancelPlacing();
return true;
}
} else if (this.shown && evt.keyCode === KEYS.del) {
this.doClearArea();
return true;
} else if (evt.keyCode === KEYS.a && evt.ctrlKey) {
this.selectAll();
event.preventDefault();
return true;
} else if (evt.keyCode === KEYS.esc && this.shown) {
this.hide();
return true;
}
return false;
}
handleMouseDown(event) {
const mainClass = event.target.classList[0];
const mousDownCallbacks = {
'ptro-crp-el': () => {
if (this.area.activated) {
if (this.imagePlaced) {
this.finishPlacing();
}
const x = (event.clientX - this.main.elLeft()) +
this.main.scroller.scrollLeft;
const y = (event.clientY - this.main.elTop()) +
this.main.scroller.scrollTop;
this.setLeft(x);
this.setTop(y);
this.setRight(this.area.el.clientWidth - x);
this.setBottom(this.area.el.clientHeight - y);
this.reCalcCropperCords();
this.area.resizingB = true;
this.area.resizingR = true;
this.hide();
}
},
'ptro-crp-rect': () => {
this.area.moving = true;
this.area.xHandle = (event.clientX - this.rectLeft()) +
this.main.scroller.scrollLeft;
this.area.yHandle = (event.clientY - this.rectTop()) +
this.main.scroller.scrollTop;
},
'ptro-crp-tr': () => {
this.area.resizingT = true;
this.area.resizingR = true;
},
'ptro-crp-br': () => {
this.area.resizingB = true;
this.area.resizingR = true;
},
'ptro-crp-bl': () => {
this.area.resizingB = true;
this.area.resizingL = true;
},
'ptro-crp-tl': () => {
this.area.resizingT = true;
this.area.resizingL = true;
},
'ptro-crp-t': () => {
this.area.resizingT = true;
},
'ptro-crp-r': () => {
this.area.resizingR = true;
},
'ptro-crp-b': () => {
this.area.resizingB = true;
},
'ptro-crp-l': () => {
this.area.resizingL = true;
},
};
if (mainClass in mousDownCallbacks) {
mousDownCallbacks[mainClass]();
if (this.imagePlaced) {
this.main.select.area.rect.style['background-image'] = `url(${this.placedDataLow})`;
}
}
}
setLeft(v) {
this.left = v;
this.area.rect.style.left = `${v}px`;
}
setRight(v) {
this.right = v;
this.area.rect.style.right = `${v}px`;
}
setTop(v) {
this.top = v;
this.area.rect.style.top = `${v}px`;
}
setBottom(v) {
this.bottom = v;
this.area.rect.style.bottom = `${v}px`;
}
handleMouseMove(event) {
if (!this.area.activated) {
return;
}
if (this.area.moving) {
let newLeft = (event.clientX - this.main.elLeft() - this.area.xHandle)
+ this.main.scroller.scrollLeft;
if (newLeft < 0) {
newLeft = 0;
} else if (newLeft + this.area.rect.clientWidth > this.area.el.clientWidth - 2) {
newLeft = this.area.el.clientWidth - this.area.rect.clientWidth - 2;
}
const hDelta = newLeft - this.left;
this.setLeft(newLeft);
this.setRight(this.right - hDelta);
let newTop = (event.clientY - this.main.elTop() - this.area.yHandle)
+ this.main.scroller.scrollTop;
if (newTop < 0) {
newTop = 0;
} else if (newTop + this.area.rect.clientHeight > this.area.el.clientHeight - 2) {
newTop = this.area.el.clientHeight - this.area.rect.clientHeight - 2;
}
const vDelta = newTop - this.top;
this.setTop(newTop);
this.setBottom(this.bottom - vDelta);
this.reCalcCropperCords();
} else {
let resizing = false;
if (this.area.resizingL) {
resizing = true;
const absLeft = this.fixCropperLeft(event.clientX + this.main.scroller.scrollLeft);
this.setLeft(absLeft - this.main.elLeft());
this.reCalcCropperCords();
}
if (this.area.resizingR) {
resizing = true;
const absRight = this.fixCropperRight(event.clientX + this.main.scroller.scrollLeft);
this.setRight(
(this.area.el.clientWidth + this.main.elLeft()) - absRight);
this.reCalcCropperCords();
}
if (this.area.resizingT) {
resizing = true;
const absTop = this.fixCropperTop(event.clientY + this.main.scroller.scrollTop);
this.setTop(absTop - this.main.elTop());
this.reCalcCropperCords();
}
if (this.area.resizingB) {
resizing = true;
const absBottom = this.fixCropperBottom(event.clientY + this.main.scroller.scrollTop);
this.setBottom(
(this.area.el.clientHeight + this.main.elTop()) - absBottom);
this.reCalcCropperCords();
}
if (this.imagePlaced && !(event.ctrlKey || event.shiftKey)) {
if (this.area.resizingT) {
if (this.area.resizingL) {
this.leftKeepRatio();
} else {
this.rightKeepRatio();
}
this.topKeepRatio();
this.reCalcCropperCords();
}
if (this.area.resizingB) {
if (this.area.resizingL) {
this.leftKeepRatio();
} else {
this.rightKeepRatio();
}
this.bottomKeepRatio();
this.reCalcCropperCords();
} if (this.area.resizingL) {
if (this.area.resizingT) {
this.topKeepRatio();
} else {
this.bottomKeepRatio();
}
this.leftKeepRatio();
this.reCalcCropperCords();
} if (this.area.resizingR) {
if (this.area.resizingT) {
this.topKeepRatio();
} else {
this.bottomKeepRatio();
}
this.rightKeepRatio();
this.reCalcCropperCords();
}
}
if (resizing && !this.shown) {
this.show();
}
if (resizing) {
clearSelection();
}
}
}
leftKeepRatio() {
const newW = this.area.rect.clientHeight * this.placedRatio;
const suggLeft = this.main.elLeft() +
(this.area.el.clientWidth - this.right - newW - 2);
const absLeft = this.fixCropperLeft(suggLeft);
this.setLeft(absLeft - this.main.elLeft());
}
topKeepRatio() {
const newH = this.area.rect.clientWidth / this.placedRatio;
const absTop = this.fixCropperTop(
this.main.elTop() + (this.area.el.clientHeight - this.bottom - newH - 2));
this.setTop(absTop - this.main.elTop());
}
bottomKeepRatio() {
const newH = this.area.rect.clientWidth / this.placedRatio;
const absBottom = this.fixCropperBottom(
this.main.elTop() +
this.top + newH + 2);
this.setBottom((this.area.el.clientHeight + this.main.elTop()) - absBottom);
}
rightKeepRatio() {
const newW = this.area.rect.clientHeight * this.placedRatio;
const absRight = this.fixCropperRight(
this.main.elLeft() +
this.left + newW + 2);
this.setRight((this.area.el.clientWidth + this.main.elLeft()) - absRight);
}
show() {
this.shown = true;
this.area.rect.removeAttribute('hidden');
}
handleMouseUp() {
if (this.area.activated) {
this.areaionCallback(!this.imagePlaced && this.area.rect.clientWidth > 0
&& this.area.rect.clientHeight > 0);
}
this.area.moving = false;
this.area.resizingT = false;
this.area.resizingR = false;
this.area.resizingB = false;
this.area.resizingL = false;
if (this.imagePlaced) {
this.main.select.area.rect.style['background-image'] = `url(${this.placedData})`;
}
}
close() {
if (this.imagePlaced) {
this.finishPlacing();
}
this.area.activated = false;
this.hide();
}
hide() {
this.area.rect.setAttribute('hidden', 'true');
this.shown = false;
this.areaionCallback(false);
}
draw() {
if (this.area.topl) {
const ratio = this.canvas.clientWidth / this.canvas.getAttribute('width');
this.setLeft(this.area.topl[0] * ratio);
this.setTop(this.area.topl[1] * ratio);
this.setRight(this.area.el.clientWidth - (
(this.area.bottoml[0] - this.area.topl[0]) * ratio));
this.setBottom(this.area.el.clientHeight - (
(this.area.bottoml[1] - this.area.topl[1]) * ratio));
}
}
rectLeft() {
return this.area.rect.documentOffsetLeft + this.main.scroller.scrollLeft;
}
rectTop() {
return this.area.rect.documentOffsetTop + this.main.scroller.scrollTop;
}
/* fixers */
fixCropperLeft(left) {
let newLeft = left;
const absLeftMiddle = this.rectLeft() + this.area.rect.clientWidth;
if (newLeft < this.main.elLeft()) {
return this.main.elLeft();
} else if (newLeft > absLeftMiddle) {
newLeft = absLeftMiddle;
if (this.area.resizingL) {
this.area.resizingL = false;
this.area.resizingR = true;
}
}
return newLeft;
}
fixCropperRight(right) {
let newRight = right;
const absRightLimit = this.main.elLeft() + this.area.el.clientWidth;
if (newRight > absRightLimit) {
return absRightLimit;
} else if (newRight < this.rectLeft()) {
newRight = this.rectLeft() +
this.area.rect.clientWidth;
if (this.area.resizingR) {
this.area.resizingR = false;
this.area.resizingL = true;
}
}
return newRight;
}
fixCropperTop(top) {
let newTop = top;
const absTopMiddle = this.rectTop() + this.area.rect.clientHeight;
if (newTop < this.main.elTop()) {
return this.main.elTop();
} else if (newTop > absTopMiddle) {
newTop = absTopMiddle;
if (this.area.resizingT) {
this.area.resizingT = false;
this.area.resizingB = true;
}
}
return newTop;
}
fixCropperBottom(bottom) {
let newBottom = bottom;
const absBottomLimit = this.main.elTop() + this.area.el.clientHeight;
if (newBottom > absBottomLimit) {
return absBottomLimit;
} else if (newBottom < this.rectTop()) {
newBottom = this.rectTop() + this.area.rect.clientHeight;
if (this.area.resizingB) {
this.area.resizingB = false;
this.area.resizingT = true;
}
}
return newBottom;
}
}