photo-editor
Version:
Simple and customizable photo editor for web applications.
714 lines (706 loc) • 21.6 kB
JavaScript
import {
Tool
} from "../chunk-W32WS4AD.js";
// src/tools/Blur.ts
import { unwrap } from "krustykrab";
var Pixel = class _Pixel {
r;
g;
b;
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
add(pixel) {
return new _Pixel(this.r + pixel.r, this.g + pixel.g, this.b + pixel.b);
}
sub(pixel) {
return new _Pixel(this.r - pixel.r, this.g - pixel.g, this.b - pixel.b);
}
mul(value) {
return new _Pixel(this.r * value, this.g * value, this.b * value);
}
div(value) {
return new _Pixel(this.r / value, this.g / value, this.b / value);
}
clone() {
return new _Pixel(this.r, this.g, this.b);
}
};
function boxesForGauss(sigma, n) {
const wIdeal = Math.sqrt(12 * sigma * sigma / n + 1);
let wl = Math.floor(wIdeal);
if (wl % 2 === 0) {
--wl;
}
const wu = wl + 2;
const mIdeal = (12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4);
const m = Math.round(mIdeal);
const sizes = [];
for (let i = 0; i < n; ++i) {
sizes.push(i < m ? wl : wu);
}
return sizes;
}
function boxBlurH(scl, tcl, w, h, r) {
const iarr = 1 / (r + r + 1);
for (let i = 0; i < h; ++i) {
let ti = i * w;
let li = ti;
let ri = ti + r;
const fv = scl[ti];
const lv = scl[ti + (w - 1)];
let val = fv.mul(r + 1);
for (let j = 0; j < r; ++j) {
val = val.add(scl[ti + j]);
}
for (let j = 0; j <= r; ++j) {
val = val.add(scl[ri].sub(fv));
++ri;
tcl[ti] = val.mul(iarr);
++ti;
}
for (let j = r + 1; j < w - r; ++j) {
val = val.add(scl[ri++].sub(scl[li]));
++li;
tcl[ti] = val.mul(iarr);
++ti;
}
for (let j = w - r; j < w; ++j) {
val = val.add(lv.sub(scl[li]));
++li;
tcl[ti] = val.mul(iarr);
++ti;
}
}
}
function boxBlurT(scl, tcl, w, h, r) {
const iarr = 1 / (r + r + 1);
for (let i = 0; i < w; ++i) {
let ti = i;
let li = ti;
let ri = ti + r * w;
const fv = scl[ti];
const lv = scl[ti + w * (h - 1)];
let val = fv.mul(r + 1);
for (let j = 0; j < r; ++j) {
val = val.add(scl[ti + j * w]);
}
for (let j = 0; j <= r; j++) {
val = val.add(scl[ri].sub(fv));
tcl[ti] = val.mul(iarr);
ri += w;
ti += w;
}
for (let j = r + 1; j < h - r; j++) {
val = val.add(scl[ri].sub(scl[li]));
tcl[ti] = val.mul(iarr);
li += w;
ri += w;
ti += w;
}
for (let j = h - r; j < h; j++) {
val = val.add(lv.sub(scl[li]));
tcl[ti] = val.mul(iarr);
li += w;
ti += w;
}
}
}
function boxBlur(scl, tcl, w, h, r) {
for (let i = 0; i < scl.length; ++i) {
tcl[i] = scl[i].clone();
}
boxBlurH(tcl, scl, w, h, r);
boxBlurT(scl, tcl, w, h, r);
}
function gaussBlur(scl, tcl, w, h, r) {
const bxs = boxesForGauss(r, 3);
boxBlur(scl, tcl, w, h, (bxs[0] - 1) / 2);
boxBlur(tcl, scl, w, h, (bxs[1] - 1) / 2);
boxBlur(scl, tcl, w, h, (bxs[2] - 1) / 2);
}
var Blur = class extends Tool {
bluring = false;
lastX = null;
lastY = null;
sigma = 3;
radius = 10;
setRadius(value) {
this.radius = Math.max(value, 1);
}
getRadius() {
return this.radius;
}
setSigma(value) {
this.sigma = Math.max(value, 0.1);
}
getSigma() {
return this.sigma;
}
onStartDraw = (event) => {
const canvas = unwrap(this.el);
this.bluring = true;
const newLastX = event.offsetX / (canvas.clientWidth / canvas.width);
const newLastY = event.offsetY / (canvas.clientHeight / canvas.height);
this.blurAtPoint(newLastX, newLastY);
this.lastX = newLastX;
this.lastY = newLastY;
};
onProcessDraw = (event) => {
if (!this.bluring) {
return;
}
const canvas = unwrap(this.el);
const newLastX = event.offsetX / (canvas.clientWidth / canvas.width);
const newLastY = event.offsetY / (canvas.clientHeight / canvas.height);
this.blurAtPoint(newLastX, newLastY);
this.lastX = newLastX;
this.lastY = newLastY;
};
onStopDraw = () => {
if (!this.bluring) {
return;
}
const canvas = unwrap(this.el);
this.bluring = false;
this.lastX = null;
this.lastY = null;
this.pushState(canvas.toDataURL());
};
blurAtPoint(_x, _y) {
const canvas = unwrap(this.el);
const ctx = unwrap(canvas.getContext("2d"));
const radius = this.getRadius();
const sigma = this.getSigma();
const x = Math.round(_x);
const y = Math.round(_y);
const { width, height } = canvas;
const startX = Math.max(x - radius, 0);
const finishX = Math.max(Math.min(x + radius, width), 0);
const startY = Math.max(y - radius, 0);
const finishY = Math.max(Math.min(y + radius, height), 0);
const newImgWidth = finishX - startX;
const newImgHeight = finishY - startY;
if (newImgWidth <= 0 || newImgHeight <= 0) {
return;
}
const newImgData = ctx.getImageData(
startX,
startY,
newImgWidth,
newImgHeight
);
const sourceChannel = [];
{
const { data } = newImgData;
for (let i = 0, l = newImgWidth * newImgHeight; i < l; ++i) {
const offset = i * 4;
sourceChannel.push(
new Pixel(data[offset], data[offset + 1], data[offset + 2])
);
}
}
const targetChannel = [];
gaussBlur(sourceChannel, targetChannel, newImgWidth, newImgHeight, sigma);
for (let i = 0, l = newImgWidth * newImgHeight; i < l; ++i) {
const { r, g, b } = targetChannel[i];
const offset = i * 4;
newImgData.data[offset] = r;
newImgData.data[offset + 1] = g;
newImgData.data[offset + 2] = b;
newImgData.data[offset + 3] = 255;
}
const bluredPiece = document.createElement("canvas");
bluredPiece.height = newImgData.height;
bluredPiece.width = newImgData.width;
const bluredPieceCtx = unwrap(bluredPiece.getContext("2d"));
bluredPieceCtx.putImageData(newImgData, 0, 0);
const pattern = ctx.createPattern(bluredPiece, "no-repeat");
ctx.save();
ctx.beginPath();
if (pattern) {
ctx.fillStyle = pattern;
}
ctx.translate(startX, startY);
ctx.arc(x - startX, y - startY, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
onAfterEnable() {
const canvas = unwrap(this.el);
canvas.addEventListener("mousedown", this.onStartDraw);
canvas.addEventListener("mousemove", this.onProcessDraw);
canvas.addEventListener("mouseup", this.onStopDraw);
}
onBeforeDisable() {
const canvas = unwrap(this.el);
this.bluring = false;
canvas.removeEventListener("mousedown", this.onStartDraw);
canvas.removeEventListener("mousemove", this.onProcessDraw);
canvas.removeEventListener("mouseup", this.onStopDraw);
}
};
// src/tools/Filter.ts
import { unwrap as unwrap2 } from "krustykrab";
var Filter = class extends Tool {
value = 0;
applied = false;
originalImageData = null;
setValue(value) {
const newValue = Math.min(Math.max(value, -1), 1);
if (newValue === this.value) {
return;
}
this.value = newValue;
this.apply();
}
getValue() {
return this.value;
}
newImageData() {
return unwrap2(this.originalImageData);
}
apply() {
const canvas = unwrap2(this.el);
const ctx = unwrap2(canvas.getContext("2d"));
ctx.putImageData(this.newImageData(), 0, 0);
if (this.applied === false) {
this.pushState(canvas.toDataURL());
this.applied = true;
} else {
this.updateState(canvas.toDataURL());
}
}
onAfterEnable() {
const canvas = unwrap2(this.el);
const ctx = unwrap2(canvas.getContext("2d"));
const { width, height } = canvas;
this.originalImageData = ctx.getImageData(0, 0, width, height);
}
onBeforeDisable() {
this.value = 0;
this.applied = false;
this.originalImageData = null;
}
};
// src/tools/Brightness.ts
var Brightness = class extends Filter {
newImageData = () => {
const value = this.getValue();
if (!this.originalImageData) {
throw new Error("Image data is not provided");
}
const { width, height, data } = this.originalImageData;
const imageData = new ImageData(width, height);
const newData = imageData.data;
const len = data.length;
const brightness = Math.round(value * 255);
for (let i = 0; i < len; i += 4) {
newData[i] = data[i] + brightness;
newData[i + 1] = data[i + 1] + brightness;
newData[i + 2] = data[i + 2] + brightness;
newData[i + 3] = data[i + 3];
}
return imageData;
};
};
// src/tools/Contrast.ts
var Contrast = class extends Filter {
newImageData = () => {
const value = this.getValue();
if (!this.originalImageData) {
throw new Error("Image data is not provided");
}
const { width, height, data } = this.originalImageData;
const imageData = new ImageData(width, height);
const newData = imageData.data;
const len = data.length;
const contrast = Math.floor(value * 255);
const contrastF = 259 * (contrast + 255) / (255 * (259 - contrast));
for (let i = 0; i < len; i += 4) {
newData[i] = contrastF * (data[i] - 128) + 128;
newData[i + 1] = contrastF * (data[i + 1] - 128) + 128;
newData[i + 2] = contrastF * (data[i + 2] - 128) + 128;
newData[i + 3] = data[i + 3];
}
return imageData;
};
};
// src/tools/Crop.ts
import { unwrap as unwrap3 } from "krustykrab";
var Crop = class extends Tool {
originalImage = null;
darkenImage = null;
cropping = false;
setted = false;
startX = null;
startY = null;
finishX = null;
finishY = null;
mousedownX = null;
mousedownY = null;
resizingBorder = null;
showCropState() {
const canvas = unwrap3(this.el);
if (this.startX === null || this.startY === null || this.finishX === null || this.finishY === null) {
throw new Error("Coordinates are not setted");
}
if (!this.darkenImage || !this.originalImage) {
throw new Error("Images are not setted");
}
const ctx = unwrap3(canvas.getContext("2d"));
const x = Math.min(this.startX, this.finishX);
const y = Math.min(this.startY, this.finishY);
const width = Math.abs(this.startX - this.finishX);
const height = Math.abs(this.startY - this.finishY);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.darkenImage, 0, 0, canvas.width, canvas.height);
if (height <= 0 || width <= 0) {
return;
}
ctx.clearRect(x, y, width, height);
ctx.drawImage(this.originalImage, x, y, width, height, x, y, width, height);
}
applyCrop() {
const canvas = unwrap3(this.el);
if (this.startX === null || this.startY === null || this.finishX === null || this.finishY === null) {
throw new Error("Coordinates are not setted");
}
const x = Math.min(this.startX, this.finishX);
const y = Math.min(this.startY, this.finishY);
const width = Math.abs(this.startX - this.finishX);
const height = Math.abs(this.startY - this.finishY);
canvas.width = width;
canvas.height = height;
const ctx = unwrap3(canvas.getContext("2d"));
ctx.drawImage(
unwrap3(this.originalImage),
x,
y,
width,
height,
0,
0,
width,
height
);
this.pushState(canvas.toDataURL());
this.originalImage = null;
this.darkenImage = null;
this.disable();
}
cancelCrop() {
const canvas = unwrap3(this.el);
this.startX = null;
this.startY = null;
this.finishX = null;
this.finishY = null;
this.setted = false;
const ctx = unwrap3(canvas.getContext("2d"));
ctx.drawImage(unwrap3(this.darkenImage), 0, 0, canvas.width, canvas.height);
this.emit("unset");
this.disable();
}
sortCoords() {
if (this.startX === null || this.startY === null || this.finishX === null || this.finishY === null) {
throw new Error("Coordinates are not setted");
}
if (this.startX > this.finishX) {
[this.startX, this.finishX] = [this.finishX, this.startX];
}
if (this.startY > this.finishY) {
[this.startY, this.finishY] = [this.finishY, this.startY];
}
}
getResizingBorder(x, y) {
if (this.startX === null || this.startY === null || this.finishX === null || this.finishY === null) {
throw new Error("Coordinates are not setted");
}
if (x > this.startX && x < this.finishX) {
if (y > this.startY - 5 && y < this.startY + 5) {
return "top";
}
if (y > this.finishY - 5 && y < this.finishY + 5) {
return "bottom";
}
}
if (y > this.startY && y < this.finishY) {
if (x > this.startX - 5 && x < this.startX + 5) {
return "left";
}
if (x > this.finishX - 5 && x < this.finishX + 5) {
return "right";
}
}
return null;
}
setCursorForResizingBorder(resizingBorder) {
const canvas = unwrap3(this.el);
switch (resizingBorder) {
case "top":
case "bottom":
canvas.style.cursor = "row-resize";
break;
case "left":
case "right":
canvas.style.cursor = "col-resize";
break;
default:
throw new Error(`Unknown border "${resizingBorder}"`);
}
}
onStartDraw = (event) => {
const canvas = unwrap3(this.el);
const x = Math.min(Math.max(event.offsetX, 0), canvas.clientWidth) / (canvas.clientWidth / canvas.width);
const y = Math.min(Math.max(event.offsetY, 0), canvas.clientHeight) / (canvas.clientHeight / canvas.height);
if (this.setted) {
const resizingBorder = this.getResizingBorder(x, y);
if (resizingBorder) {
this.resizingBorder = resizingBorder;
this.setCursorForResizingBorder(resizingBorder);
return;
}
}
this.cropping = true;
this.mousedownX = x;
this.mousedownY = y;
};
onProcessDraw = (event) => {
const canvas = unwrap3(this.el);
const x = Math.min(Math.max(event.offsetX, 0), canvas.clientWidth) / (canvas.clientWidth / canvas.width);
const y = Math.min(Math.max(event.offsetY, 0), canvas.clientHeight) / (canvas.clientHeight / canvas.height);
if (this.mousedownX !== null && this.mousedownY !== null) {
this.startX = this.mousedownX;
this.startY = this.mousedownY;
this.mousedownX = null;
this.mousedownY = null;
}
if (this.resizingBorder) {
switch (this.resizingBorder) {
case "top":
this.startY = y;
break;
case "bottom":
this.finishY = y;
break;
case "left":
this.startX = x;
break;
case "right":
this.finishX = x;
break;
default:
throw new Error(`Unknown border "${this.resizingBorder}"`);
}
this.showCropState();
return;
}
if (this.cropping) {
this.finishX = x;
this.finishY = y;
this.showCropState();
return;
}
if (this.setted) {
const resizingBorder = this.getResizingBorder(x, y);
if (resizingBorder) {
this.setCursorForResizingBorder(resizingBorder);
} else {
canvas.style.removeProperty("cursor");
}
}
};
onStopDraw = () => {
const canvas = unwrap3(this.el);
this.mousedownX = null;
this.mousedownY = null;
if (this.resizingBorder) {
this.resizingBorder = null;
canvas.style.removeProperty("cursor");
this.sortCoords();
return;
}
if (this.cropping) {
this.sortCoords();
this.cropping = false;
this.setted = true;
this.emit("set");
}
};
onAfterEnable() {
const canvas = unwrap3(this.el);
const { width, height } = canvas;
this.originalImage = document.createElement("canvas");
this.originalImage.width = width;
this.originalImage.height = height;
const originalContext = unwrap3(this.originalImage.getContext("2d"));
originalContext.drawImage(canvas, 0, 0);
this.darkenImage = document.createElement("canvas");
this.darkenImage.width = width;
this.darkenImage.height = height;
const darkenCtx = unwrap3(this.darkenImage.getContext("2d"));
darkenCtx.drawImage(canvas, 0, 0);
darkenCtx.fillStyle = "rgba(0, 0, 0, 0.4)";
darkenCtx.fillRect(0, 0, width, height);
canvas.addEventListener("mousedown", this.onStartDraw);
canvas.addEventListener("mousemove", this.onProcessDraw);
canvas.addEventListener("mouseup", this.onStopDraw);
}
onBeforeDisable() {
const canvas = unwrap3(this.el);
this.cropping = false;
this.setted = false;
this.resizingBorder = null;
canvas.style.removeProperty("cursor");
if (this.darkenImage) {
const ctx = unwrap3(canvas.getContext("2d"));
ctx.drawImage(unwrap3(this.originalImage), 0, 0);
this.originalImage = null;
this.darkenImage = null;
}
canvas.removeEventListener("mousedown", this.onStartDraw);
canvas.removeEventListener("mousemove", this.onProcessDraw);
canvas.removeEventListener("mouseup", this.onStopDraw);
}
};
// src/tools/Rectangle.ts
import { unwrap as unwrap4 } from "krustykrab";
var Rectangle = class extends Tool {
originalImage = null;
drawing = false;
startX = null;
startY = null;
finishX = null;
finishY = null;
showRectangle() {
const canvas = unwrap4(this.el);
const ctx = unwrap4(canvas.getContext("2d"));
if (this.startX === null || this.startY === null || this.finishX === null || this.finishY === null) {
throw new Error("Coordinates are not setted");
}
const x = Math.min(this.startX, this.finishX);
const y = Math.min(this.startY, this.finishY);
const width = Math.abs(this.startX - this.finishX);
const height = Math.abs(this.startY - this.finishY);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(
unwrap4(this.originalImage),
0,
0,
canvas.width,
canvas.height
);
ctx.beginPath();
ctx.lineWidth = 4;
ctx.strokeStyle = "#CB0000";
ctx.rect(x, y, width, height);
ctx.stroke();
}
onStartDraw = (event) => {
const canvas = unwrap4(this.el);
const x = event.offsetX / (canvas.clientWidth / canvas.width);
const y = event.offsetY / (canvas.clientHeight / canvas.height);
this.drawing = true;
this.startX = x;
this.startY = y;
};
onProcessDraw = (event) => {
const canvas = unwrap4(this.el);
const x = event.offsetX / (canvas.clientWidth / canvas.width);
const y = event.offsetY / (canvas.clientHeight / canvas.height);
if (this.drawing) {
this.finishX = x;
this.finishY = y;
this.showRectangle();
}
};
onStopDraw = () => {
if (!this.drawing) {
return;
}
const canvas = unwrap4(this.el);
this.drawing = false;
const originalCtx = unwrap4(unwrap4(this.originalImage).getContext("2d"));
originalCtx.drawImage(canvas, 0, 0);
originalCtx.drawImage(canvas, 0, 0);
this.pushState(canvas.toDataURL());
};
onAfterEnable() {
const canvas = unwrap4(this.el);
const { width, height } = canvas;
this.originalImage = document.createElement("canvas");
this.originalImage.width = width;
this.originalImage.height = height;
const originalCtx = unwrap4(this.originalImage.getContext("2d"));
originalCtx.drawImage(canvas, 0, 0);
canvas.addEventListener("mousedown", this.onStartDraw);
canvas.addEventListener("mousemove", this.onProcessDraw);
canvas.addEventListener("mouseup", this.onStopDraw);
}
onBeforeDisable() {
const canvas = unwrap4(this.el);
this.drawing = false;
this.originalImage = null;
canvas.removeEventListener("mousedown", this.onStartDraw);
canvas.removeEventListener("mousemove", this.onProcessDraw);
canvas.removeEventListener("mouseup", this.onStopDraw);
}
};
// src/tools/RotateLeft.ts
import { unwrap as unwrap5 } from "krustykrab";
var RotateLeft = class extends Tool {
onAfterEnable() {
const canvas = unwrap5(this.el);
const ctx = unwrap5(canvas.getContext("2d"));
const { width, height } = canvas;
const otherCanvas = document.createElement("canvas");
otherCanvas.width = height;
otherCanvas.height = width;
const otherCtx = unwrap5(otherCanvas.getContext("2d"));
otherCtx.clearRect(0, 0, width, height);
otherCtx.translate(height / 2, width / 2);
otherCtx.rotate(-Math.PI / 2);
otherCtx.translate(-width / 2, -height / 2);
otherCtx.drawImage(canvas, 0, 0);
canvas.width = height;
canvas.height = width;
ctx.drawImage(otherCanvas, 0, 0);
this.pushState(canvas.toDataURL());
this.disable();
}
};
// src/tools/RotateRight.ts
import { unwrap as unwrap6 } from "krustykrab";
var RotateRight = class extends Tool {
onAfterEnable() {
const canvas = unwrap6(this.el);
const ctx = unwrap6(canvas.getContext("2d"));
const { width, height } = canvas;
const otherCanvas = document.createElement("canvas");
otherCanvas.width = height;
otherCanvas.height = width;
const otherCtx = unwrap6(otherCanvas.getContext("2d"));
otherCtx.clearRect(0, 0, width, height);
otherCtx.translate(height / 2, width / 2);
otherCtx.rotate(Math.PI / 2);
otherCtx.translate(-width / 2, -height / 2);
otherCtx.drawImage(canvas, 0, 0);
canvas.width = height;
canvas.height = width;
ctx.drawImage(otherCanvas, 0, 0);
this.pushState(canvas.toDataURL());
this.disable();
}
};
export {
Blur,
Brightness,
Contrast,
Crop,
Rectangle,
RotateLeft,
RotateRight
};
//# sourceMappingURL=index.js.map