painterro
Version:
HTML5 image editing widget (js paint plugin)
358 lines (324 loc) • 11.8 kB
JavaScript
export default class PrimitiveTool {
constructor(main) {
this.ctx = main.ctx;
this.el = main.toolContainer;
this.main = main;
this.helperCanvas = document.createElement('canvas');
this.canvas = main.canvas;
}
activate(type) {
this.type = type;
this.state = {};
if (type === 'line' || type === 'brush' || type === 'eraser' || type === 'arrow') {
this.ctx.lineJoin = 'round';
} else {
this.ctx.lineJoin = 'miter';
}
}
setLineWidth(width) {
if (`${width}`.match(/^\d+$/)) {
this.lineWidth = +width;
} else {
throw Error(`WARN: STR "${width}" is not an int`);
}
}
setShadowOn(state) {
this.shadowOn = state;
}
setArrowLength(length) {
this.arrowLength = length;
}
setEraserWidth(width) {
this.eraserWidth = width;
}
handleMouseDown(event) {
this.activate(this.type);
const mainClass = event.target.classList[0];
this.ctx.lineWidth = this.lineWidth;
this.ctx.strokeStyle = this.main.colorWidgetState.line.alphaColor;
this.ctx.fillStyle = this.main.colorWidgetState.fill.alphaColor;
const scale = this.main.getScale();
this.ctx.lineCap = 'round';
if (mainClass === 'ptro-crp-el' || mainClass === 'ptro-zoomer') {
this.tmpData = this.ctx.getImageData(0, 0, this.main.size.w, this.main.size.h);
if (this.type === 'brush' || this.type === 'eraser') {
this.state.cornerMarked = true;
const cord = [
(event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
(event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
];
const cur = {
x: cord[0] * scale,
y: cord[1] * scale,
};
this.points = [cur];
this.drawBrushPath();
} else {
this.state.cornerMarked = true;
this.centerCord = [
(event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
(event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
];
this.centerCord = [this.centerCord[0] * scale, this.centerCord[1] * scale];
}
}
}
drawBrushPath() {
const smPoints = this.points;
let lineFill;
const origComposition = this.ctx.globalCompositeOperation;
const isEraser = this.type === 'eraser';
lineFill = this.main.colorWidgetState.line.alphaColor;
const bgIsTransparent = this.main.currentBackgroundAlpha !== 1.0;
for (let i = 1; i <= (isEraser && bgIsTransparent ? 2 : 1); i += 1) {
if (isEraser) {
this.ctx.globalCompositeOperation = i === 1 && bgIsTransparent ? 'destination-out' : origComposition;
lineFill = i === 1 && bgIsTransparent ? 'rgba(0,0,0,1)' : this.main.currentBackground;
}
if (smPoints.length === 1) {
this.ctx.beginPath();
this.ctx.lineWidth = 0;
this.ctx.fillStyle = lineFill;
this.ctx.arc(
this.points[0].x, this.points[0].y,
this.lineWidth / 2, this.lineWidth / 2,
0, 2 * Math.PI);
this.ctx.fill();
this.ctx.closePath();
} else {
this.ctx.beginPath();
if (this.type === 'eraser') {
this.ctx.lineWidth = this.eraserWidth;
} else {
this.ctx.lineWidth = this.lineWidth;
}
this.ctx.strokeStyle = lineFill;
this.ctx.fillStyle = this.main.colorWidgetState.fill.alphaColor;
this.ctx.moveTo(this.points[0].x, this.points[0].y);
let last;
smPoints.slice(1).forEach((p) => {
this.ctx.lineTo(p.x, p.y);
last = p;
});
if (last) {
this.ctx.moveTo(last.x, last.y);
}
this.ctx.stroke();
this.ctx.closePath();
}
}
this.ctx.globalCompositeOperation = origComposition;
}
handleMouseMove(event) {
const ctx = this.ctx;
if (this.state.cornerMarked) {
this.ctx.putImageData(this.tmpData, 0, 0);
this.curCord = [
(event.clientX - this.main.elLeft()) + this.main.scroller.scrollLeft,
(event.clientY - this.main.elTop()) + this.main.scroller.scrollTop,
];
const scale = this.main.getScale();
this.curCord = [this.curCord[0] * scale, this.curCord[1] * scale];
if (this.type === 'brush' || this.type === 'eraser') {
// const prevLast = this.points.slice(-1)[0];
const cur = {
x: this.curCord[0],
y: this.curCord[1],
};
this.points.push(cur);
this.drawBrushPath();
} else if (this.type === 'line') {
if (event.ctrlKey || event.shiftKey) {
const deg = (Math.atan(
-(this.curCord[1] - this.centerCord[1]) / (this.curCord[0] - this.centerCord[0]),
) * 180) / Math.PI;
if (Math.abs(deg) < 45.0 / 2) {
this.curCord[1] = this.centerCord[1];
} else if (Math.abs(deg) > 45.0 + (45.0 / 2)) {
this.curCord[0] = this.centerCord[0];
} else {
const base = (Math.abs(this.curCord[0] - this.centerCord[0])
- Math.abs(this.centerCord[1] - this.curCord[1])) / 2;
this.curCord[0] -= base * (this.centerCord[0] < this.curCord[0] ? 1 : -1);
this.curCord[1] -= base * (this.centerCord[1] > this.curCord[1] ? 1 : -1);
}
}
ctx.beginPath();
ctx.moveTo(this.centerCord[0], this.centerCord[1]);
ctx.lineTo(this.curCord[0], this.curCord[1]);
ctx.closePath();
const origShadowColor = ctx.shadowColor;
if (this.shadowOn) {
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = this.lineWidth;
ctx.shadowOffsetX = this.lineWidth / 2.0;
ctx.shadowOffsetY = this.lineWidth / 2.0;
}
ctx.stroke();
ctx.shadowColor = origShadowColor;
} else if (this.type === 'arrow') {
let deg = (Math.atan(
-(this.curCord[1] - this.centerCord[1]) / (this.curCord[0] - this.centerCord[0]),
) * 180) / Math.PI;
if (event.ctrlKey || event.shiftKey) {
if (Math.abs(deg) < 45.0 / 2) {
this.curCord[1] = this.centerCord[1];
} else if (Math.abs(deg) > 45.0 + (45.0 / 2)) {
this.curCord[0] = this.centerCord[0];
} else {
const base = (Math.abs(this.curCord[0] - this.centerCord[0])
- Math.abs(this.centerCord[1] - this.curCord[1])) / 2;
this.curCord[0] -= base * (this.centerCord[0] < this.curCord[0] ? 1 : -1);
this.curCord[1] -= base * (this.centerCord[1] > this.curCord[1] ? 1 : -1);
}
}
if (this.curCord[0] < this.centerCord[0]) {
deg = (180 + deg);
}
this.ctx.beginPath();
const origCap = this.ctx.lineCap;
const origFill = this.ctx.fillStyle;
this.ctx.fillStyle = this.main.colorWidgetState.line.alphaColor;
this.ctx.lineCap = 'square';
const r = Math.min(this.arrowLength, 0.9 * Math.sqrt(
((this.centerCord[0] - this.curCord[0]) ** 2) +
((this.centerCord[1] - this.curCord[1]) ** 2)));
const fromx = this.centerCord[0];
const fromy = this.centerCord[1];
const tox = this.curCord[0];
const toy = this.curCord[1];
const xCenter = this.curCord[0];
const yCenter = this.curCord[1];
let angle;
let x;
let y;
angle = Math.atan2(toy - fromy, tox - fromx);
x = (r * Math.cos(angle)) + xCenter;
y = (r * Math.sin(angle)) + yCenter;
this.ctx.moveTo(x, y);
angle += (1.0 / 3) * (2 * Math.PI);
x = (r * Math.cos(angle)) + xCenter;
y = (r * Math.sin(angle)) + yCenter;
this.ctx.lineTo(x, y);
const xTail1 = xCenter + ((x - xCenter) / 3.0);
const yTail1 = yCenter + ((y - yCenter) / 3.0);
ctx.lineTo(xTail1, yTail1);
ctx.lineTo(this.centerCord[0], this.centerCord[1]);
angle += (1.0 / 3) * (2 * Math.PI);
x = (r * Math.cos(angle)) + xCenter;
y = (r * Math.sin(angle)) + yCenter;
const xTail2 = xCenter + ((x - xCenter) / 3.0);
const yTail2 = yCenter + ((y - yCenter) / 3.0);
ctx.lineTo(xTail2, yTail2);
ctx.lineTo(x, y);
ctx.closePath();
const origShadowColor = ctx.shadowColor;
if (this.shadowOn) {
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = Math.log(r) * this.main.params.shadowScale;
ctx.shadowOffsetX = Math.log10(r);
ctx.shadowOffsetY = Math.log10(r);
}
ctx.fill();
ctx.lineCap = origCap;
ctx.fillStyle = origFill;
ctx.shadowColor = origShadowColor;
} else if (this.type === 'rect') {
ctx.beginPath();
const tl = [
this.centerCord[0],
this.centerCord[1]];
let w = this.curCord[0] - this.centerCord[0];
let h = this.curCord[1] - this.centerCord[1];
if (event.ctrlKey || event.shiftKey) {
const min = Math.min(Math.abs(w), Math.abs(h));
w = min * Math.sign(w);
h = min * Math.sign(h);
}
const halfLW = this.lineWidth / 2.0;
// normalize fix half compensation
if (w < 0) {
tl[0] += w;
w = -w;
}
if (h < 0) {
tl[1] += h;
h = -h;
}
this.ctx.rect(
tl[0] + halfLW,
tl[1] + halfLW,
(w - this.lineWidth),
(h - this.lineWidth));
this.ctx.fill();
const origShadowColor = ctx.shadowColor;
if (this.shadowOn) {
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = this.lineWidth;
ctx.shadowOffsetX = this.lineWidth / 2.0;
ctx.shadowOffsetY = this.lineWidth / 2.0;
}
if (this.lineWidth) {
// TODO: no shadow on unstroked, do we need it?
this.ctx.strokeRect(tl[0], tl[1], w, h);
}
ctx.shadowColor = origShadowColor;
this.ctx.closePath();
} else if (this.type === 'ellipse') {
this.ctx.beginPath();
const x1 = this.centerCord[0];
const y1 = this.centerCord[1];
let w = this.curCord[0] - x1;
let h = this.curCord[1] - y1;
if (event.ctrlKey || event.shiftKey) {
const min = Math.min(Math.abs(w), Math.abs(h));
w = min * Math.sign(w);
h = min * Math.sign(h);
}
const rX = Math.abs(w);
const rY = Math.abs(h);
const tlX = Math.min(x1, x1 + w);
const tlY = Math.min(y1, y1 + h);
this.ctx.save();
let xScale = 1;
let yScale = 1;
let radius;
const hR = rX / 2;
const vR = rY / 2;
if (rX > rY) {
yScale = rX / rY;
radius = hR;
} else {
xScale = rY / rX;
radius = vR;
}
this.ctx.scale(1 / xScale, 1 / yScale);
this.ctx.arc(
(tlX + hR) * xScale,
(tlY + vR) * yScale,
radius, 0, 2 * Math.PI);
this.ctx.restore();
this.ctx.fill();
const origShadowColor = ctx.shadowColor;
if (this.shadowOn) {
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = this.lineWidth;
ctx.shadowOffsetX = this.lineWidth / 2.0;
ctx.shadowOffsetY = this.lineWidth / 2.0;
}
ctx.stroke();
ctx.shadowColor = origShadowColor;
this.ctx.beginPath();
}
}
}
handleMouseUp() {
if (this.state.cornerMarked) {
this.state.cornerMarked = false;
this.main.worklog.captureState();
}
}
setPixelSize(size) {
this.pixelSize = size;
}
}