/**
* Interface for Drawable objects to adhere to.
* All Drawable objects must implement the following classes
* so that the Canvas object can call on them.
*/
export class Drawable {
/**
* Constructor to initialize initial values.
*
* @constructor
*/
constructor() { this.selected = false; }
/**
* Initializes initial values that depend on positionality.
* Distinct from constructor in that it relies on a mousedown
* event to tell it where this object is located.
*
* @param {MouseEvent} e The mousedown event
* @returns {boolean} Whether to continue dragging this object
* after the initial click
*/
init(e) { return false; }
/**
* Handles what happens when an object is clicked at first.
*
* @param {MouseEvent} e The mousedown event
* @returns {boolean} Whether to continue dragging this object
* after the initial click
*/
onMouseDown(e) { return false; }
/**
* Handles what happens to the object when the users drags
*
* @param {MouseEvent} e The drag event
*/
onDrag(e) {}
/**
* Handles what happens when the user lets go of their mouse.
*
* @param {MouseEvent} e The mouseup event
* @return {boolean} Whether to have this object be selected
* after letting go
*/
onMouseUp(e) { return false; }
/**
* Draws self on the canvas screen.
*
* @param {*} ctx The canvas context to draw with
*/
drawSelf(ctx) {}
/**
* Draws this objects selection box. To be used when
* an object is selected.
*
* @param {*} ctx The canvas context to draw with
*/
drawFocus(ctx) {}
/**
* Returns whether a click is over this object
*
* @param {Number} x The x position to compare against
* @param {Number} y The y position to compare against
* @returns {boolean} Whether the click is over this object
*/
overSelf(x, y) { return false; }
/**
* Same as overSelf, except for if its over the selected bounding
* box in particular. Used for objects that have a slightly larger
* selection box and to prioritize the selected object, even if it's
* below other objects on screen.
*
* @param {Number} x The x position to compare against
* @param {Number} y The y position to compare against
* @returns {boolean} Whether the click is over this bounding box
*/
overSelection(x, y) { return false; }
/**
* Moves an object by dx pixels horizontally on the screen
* @param {Number} dx The amount to move
*/
moveX(dx) {}
/**
* Moves an objects by dy pixels vertically on the screen
* @param {Number} dy The amount to move
*/
moveY(dy) {}
/**
* Exports this Drawable as a dictionary of values
*/
export() {}
/**
* Imports values from a dictionary into this drawable
*
* @param {*} data What to import
*/
import(data) {}
}
/**
* Creates a box on the screen. Can be locked into shape by holding shift.
*/
export class Box extends Drawable {
constructor(parent) {
super();
this.parent = parent;
this.canvas = this.parent.canvas;
this.r = 41;
this.g = 165;
this.b = 62;
}
init(e) {
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
this.startMouseX = e.clientX;
this.startMouseY = e.clientY;
this.x1 = (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX;
this.y1 = (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY;
this.x2 = (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX;
this.y2 = (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY;
this.startX1 = this.x1;
this.startY1 = this.y1;
this.startX2 = this.x2;
this.startY2 = this.y2;
this.selectionID = 3; // bottom right corner
return true;
}
onMouseDown(e) {
this.selected = true;
this.startMouseX = e.clientX;
this.startMouseY = e.clientY;
this.startX1 = this.x1;
this.startY1 = this.y1;
this.startX2 = this.x2;
this.startY2 = this.y2;
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
this.selectionID = this.overSelection(
(e.clientX - this.canvas.getBoundingClientRect().x) * scaleX,
(e.clientY - this.canvas.getBoundingClientRect().y) * scaleY
);
return true;
}
onDrag(e) {
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
let xi = 0; let yi = 0;
switch (this.selectionID) {
case 0: xi = 3; yi = 3; break;
case 1: xi = 1; yi = 1; break;
case 2: xi = 1; yi = 2; break;
case 3: xi = 2; yi = 2; break; // bottom right
case 4: xi = 2; yi = 1; break;
case 5: xi = 0; yi = 1; break;
case 6: xi = 0; yi = 2; break;
case 7: xi = 1; yi = 0; break;
case 8: xi = 2; yi = 0; break;
case 9: break; // rotate, TBD
}
const mouseX = (e.clientX - this.startMouseX) * scaleX;
const mouseY = (e.clientY - this.startMouseY) * scaleY;
if (xi % 2 === 1) { this.x1 = this.startX1 + mouseX; }
if (yi % 2 === 1) { this.y1 = this.startY1 + mouseY; }
if (xi / 2 >= 1) { this.x2 = this.startX2 + mouseX; }
if (yi / 2 >= 1) { this.y2 = this.startY2 + mouseY; }
// for snapping to aspect ratio
if (this.parent.shiftHeld && xi % 3 !== 0 && yi % 3 !== 0) {
if (Math.abs(this.x1 - this.x2) >= Math.abs(this.y1 - this.y2)) {
const dx = Math.abs(this.x1 - this.x2);
if (yi % 2 === 1) {
this.y1 = this.startY2 + dx *
(this.startY2 > (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY ? -1 : 1);
} else {
this.y2 = this.startY1 + dx *
(this.startY1 > (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY ? -1 : 1);
}
} else {
const dy = Math.abs(this.y1 - this.y2);
if (xi % 2 === 1) {
this.x1 = this.startX2 + dy *
(this.startX2 > (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX ? -1 : 1);
} else {
this.x2 = this.startX1 + dy *
(this.startX1 > (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX ? -1 : 1);
}
}
}
}
onMouseUp(e) {
return true;
}
drawSelf(ctx) {
ctx.fillStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
ctx.fillRect(this.x1, this.y1, this.x2 - this.x1, this.y2 - this.y1);
}
drawFocus(ctx) {
if (!this.selected) { return; }
const minX = Math.min(this.x1, this.x2);
const minY = Math.min(this.y1, this.y2);
const width = Math.abs(this.x2 - this.x1);
const height = Math.abs(this.y2 - this.y1);
ctx.strokeStyle = 'rgb(200, 200, 255)';
ctx.lineWidth = 4;
ctx.strokeRect(minX - 2, minY - 2, width + 4, height + 4);
ctx.strokeStyle = 'rgb(50, 50, 255)';
ctx.lineWidth = 2;
ctx.strokeRect(minX, minY, width, height);
ctx.beginPath();
ctx.moveTo((this.x1 + this.x2) / 2, minY);
ctx.lineTo((this.x1 + this.x2) / 2, minY - 40);
ctx.stroke();
ctx.beginPath();
ctx.arc((this.x1 + this.x2) / 2, minY - 40, 7, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(50, 50, 255)';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(255, 255, 255)';
ctx.stroke();
const pts = [[this.x1, this.y1], [this.x1, this.y2], [this.x2, this.y2], [this.x2, this.y1],
[(this.x1 + this.x2) / 2, this.y1], [(this.x1 + this.x2) / 2, this.y2],
[this.x1, (this.y1 + this.y2) / 2], [this.x2, (this.y1 + this.y2) / 2]];
for (let pt = 0; pt < pts.length; pt++) {
ctx.fillStyle = 'rgb(255, 255, 255)';
ctx.fillRect(pts[pt][0] - 7, pts[pt][1] - 7, 15, 15);
ctx.fillStyle = 'rgb(50, 50, 255)';
ctx.fillRect(pts[pt][0] - 5, pts[pt][1] - 5, 11, 11);
}
}
overSelf(x, y) {
const minX = Math.min(this.x1, this.x2);
const maxX = Math.max(this.x1, this.x2);
const minY = Math.min(this.y1, this.y2);
const maxY = Math.max(this.y1, this.y2);
return minX <= x && x <= maxX && minY <= y && y <= maxY;
}
overSelection(x, y) {
const pts = [[this.x1, this.y1], [this.x1, this.y2], [this.x2, this.y2], [this.x2, this.y1],
[(this.x1 + this.x2) / 2, this.y1], [(this.x1 + this.x2) / 2, this.y2],
[this.x1, (this.y1 + this.y2) / 2], [this.x2, (this.y1 + this.y2) / 2],
[(this.x1 + this.x2) / 2, Math.min(this.y1, this.y2) - 40]];
for (let pt = 0; pt < pts.length; pt++) {
const dx = Math.abs(pts[pt][0] - x);
const dy = Math.abs(pts[pt][1] - y);
if (dx <= 7 && dy <= 7) { return pt + 1; }
}
return 0;
}
moveX(dx) {
this.x1 += dx;
this.x2 += dx;
}
moveY(dy) {
this.y1 += dy;
this.y2 += dy;
}
export() {
return {
x1: this.x1,
y1: this.y1,
x2: this.x2,
y2: this.y2,
r: this.r,
g: this.g,
b: this.b
};
}
import(data) {
if (!data) return;
if ('x1' in data && typeof data.x1 === 'number') this.x1 = data.x1;
if ('y1' in data && typeof data.y1 === 'number') this.y1 = data.y1;
if ('x2' in data && typeof data.x2 === 'number') this.x2 = data.x2;
if ('y2' in data && typeof data.y2 === 'number') this.y2 = data.y2;
if ('r' in data && typeof data.r === 'number') this.r = data.r;
if ('g' in data && typeof data.g === 'number') this.g = data.g;
if ('b' in data && typeof data.b === 'number') this.b = data.b;
}
}
/**
* Draws an image on the screen. Extends Box since it's
* effectively the same thing except it uses an image
* instead of color to draw.
*/
export class Image extends Box {
constructor(parent, src) {
super(parent);
if (src) { this.src = src; } else if (this.parent.toolbar) {
const info = this.parent.toolbar.getToolInfo();
if ('src' in info) { this.src = info.src; }
}
this.img = document.createElement('img');
this.img.src = this.src;
}
init(e) {
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
this.x1 = (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX;
this.y1 = (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY;
if (this.parent.toolbar) {
const startupInfo = this.parent.toolbar.getToolInfo();
this.x2 = this.x1 + startupInfo.width;
this.y2 = this.y1 + startupInfo.height;
}
return false;
}
drawSelf(ctx) {
ctx.drawImage(this.img, this.x1, this.y1, this.x2 - this.x1, this.y2 - this.y1);
}
export() {
return {
x1: this.x1,
y1: this.y1,
x2: this.x2,
y2: this.y2,
src: this.src
};
}
import(data) {
if (!data) return;
if ('x1' in data && typeof data.x1 === 'number') this.x1 = data.x1;
if ('y1' in data && typeof data.y1 === 'number') this.y1 = data.y1;
if ('x2' in data && typeof data.x2 === 'number') this.x2 = data.x2;
if ('y2' in data && typeof data.y2 === 'number') this.y2 = data.y2;
if ('src' in data && typeof data.src === 'string') {
this.src = data.src;
this.img.src = this.src;
}
}
}
/**
* Draws a textbox on the screen. Has the ability to change font style,
* color, and size, as well as be italicized and bolded.
*/
export class Textbox extends Drawable {
constructor(parent) {
super(parent);
this.parent = parent;
this.canvas = this.parent.canvas;
this.r = 0;
this.g = 0;
this.b = 0;
const startupInfo = this.parent.toolbar.getToolInfo();
this.text = startupInfo.text;
this.fontSize = startupInfo.fontSize;
this.fontStyle = startupInfo.fontStyle;
this.bold = false;
this.italics = false;
this.underline = false;
}
init(e) {
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
this.x = (e.clientX - this.canvas.getBoundingClientRect().x) * scaleX;
this.y = (e.clientY - this.canvas.getBoundingClientRect().y) * scaleY;
return false;
}
onMouseDown(e) {
this.selected = true;
this.lastX = e.clientX;
this.lastY = e.clientY;
return true;
}
onDrag(e) {
const scaleX = this.canvas.width / this.canvas.getBoundingClientRect().width;
const scaleY = this.canvas.height / this.canvas.getBoundingClientRect().height;
this.x += (e.clientX - this.lastX) * scaleX;
this.y += (e.clientY - this.lastY) * scaleY;
this.lastX = e.clientX;
this.lastY = e.clientY;
}
onMouseUp(e) {
return this.selected;
}
overSelf(x, y) {
this.canvas.getContext('2d').font = `${this.bold ? ' bold' : ''} ${this.italics ? ' italic' : ''} ${this.fontSize}px ${this.fontStyle}`;
return this.x <= x && x <= this.x + this.canvas.getContext('2d').measureText(this.text).width && this.y <= y + this.fontSize && y <= this.y;
}
drawSelf(ctx) {
ctx.font = `${this.bold ? ' bold' : ''} ${this.italics ? ' italic' : ''} ${this.fontSize}px ${this.fontStyle}`;
ctx.fillStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
ctx.fillText(this.text, this.x, this.y);
if (this.underline) {
ctx.strokeStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
ctx.lineWidth = this.fontSize / 10;
ctx.beginPath();
ctx.moveTo(this.x, this.y + this.fontSize / 8);
ctx.lineTo(this.x + ctx.measureText(this.text).width, this.y + this.fontSize / 8);
ctx.stroke();
}
}
drawFocus(ctx) {
if (!this.selected) { return; }
ctx.font = `${this.bold ? ' bold' : ''} ${this.italics ? ' italic' : ''} ${this.fontSize}px ${this.fontStyle}`;
ctx.fillStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
const text = ctx.measureText(this.text);
const minX = this.x;
const width = text.width;
const height = this.fontSize;
const minY = this.y - height;
ctx.strokeStyle = 'rgb(200, 200, 255)';
ctx.lineWidth = 4;
ctx.strokeRect(minX - 7, minY + 3, width + 14, height + 4);
ctx.strokeStyle = 'rgb(50, 50, 255)';
ctx.lineWidth = 2;
ctx.strokeRect(minX - 5, minY + 5, width + 10, height);
}
moveX(dx) {
this.x += dx;
}
moveY(dy) {
this.y += dy;
}
export() {
return {
x: this.x,
y: this.y,
text: this.text,
style: this.fontStyle,
size: this.fontSize,
bold: this.bold,
italics: this.italics,
r: this.r,
g: this.g,
b: this.b
};
}
import(data) {
if (!data) return;
if ('x' in data && typeof data.x === 'number') this.x = data.x;
if ('y' in data && typeof data.y === 'number') this.y = data.y;
if ('text' in data && typeof data.text === 'string') this.text = data.text;
if ('style' in data && typeof data.style === 'string') this.fontStyle = data.style;
if ('size' in data && typeof data.size === 'number') this.fontSize = data.size;
if ('bold' in data && typeof data.bold === 'boolean') this.bold = data.bold;
if ('italics' in data && typeof data.italics === 'boolean') this.italics = data.italics;
if ('underline' in data && typeof data.underline === 'boolean') this.underline = data.underline;
if ('r' in data && typeof data.r === 'number') this.r = data.r;
if ('g' in data && typeof data.g === 'number') this.g = data.g;
if ('b' in data && typeof data.b === 'number') this.b = data.b;
}
}
/**
* Draws an ellipse on the screen. Extends Box since it's
* effectively the same thing except the shape is slightly different.
* Also has a slightly different selection render.
*/
export class Ellipse extends Box {
drawSelf(ctx) {
ctx.fillStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
ctx.beginPath();
ctx.ellipse((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2,
Math.abs(this.x2 - this.x1) / 2, Math.abs(this.y2 - this.y1) / 2, 0, 0, 2 * Math.PI);
ctx.fill();
}
drawFocus(ctx) { // almost the same as for box but different border
if (!this.selected) { return; }
const minX = Math.min(this.x1, this.x2);
const minY = Math.min(this.y1, this.y2);
const width = Math.abs(this.x2 - this.x1);
const height = Math.abs(this.y2 - this.y1);
ctx.strokeStyle = 'rgb(200, 200, 255)';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.ellipse((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2,
(Math.abs(this.x2 - this.x1) + 2) / 2, (Math.abs(this.y2 - this.y1) + 2) / 2,
0, 0, 2 * Math.PI);
ctx.stroke();
ctx.strokeStyle = 'rgb(50, 50, 255)';
ctx.lineWidth = 2;
ctx.strokeRect(minX, minY, width, height);
ctx.beginPath();
ctx.moveTo((this.x1 + this.x2) / 2, minY);
ctx.lineTo((this.x1 + this.x2) / 2, minY - 40);
ctx.stroke();
ctx.beginPath();
ctx.arc((this.x1 + this.x2) / 2, minY - 40, 7, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(50, 50, 255)';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(255, 255, 255)';
ctx.stroke();
const pts = [[this.x1, this.y1], [this.x1, this.y2], [this.x2, this.y2], [this.x2, this.y1],
[(this.x1 + this.x2) / 2, this.y1], [(this.x1 + this.x2) / 2, this.y2],
[this.x1, (this.y1 + this.y2) / 2], [this.x2, (this.y1 + this.y2) / 2]];
for (let pt = 0; pt < pts.length; pt++) {
ctx.fillStyle = 'rgb(255, 255, 255)';
ctx.fillRect(pts[pt][0] - 7, pts[pt][1] - 7, 15, 15);
ctx.fillStyle = 'rgb(50, 50, 255)';
ctx.fillRect(pts[pt][0] - 5, pts[pt][1] - 5, 11, 11);
}
}
overSelf(x, y) {
const centerX = (this.x1 + this.x2) / 2;
const centerY = (this.y1 + this.y2) / 2;
const rx = Math.abs(this.x2 - this.x1) / 2;
const ry = Math.abs(this.y2 - this.y1) / 2;
return ((centerX - x) / rx) ** 2 + ((centerY - y) / ry) ** 2 <= 1;
}
}