/**
* canvas-layers - v2.1.4
* A toolbox that makes it easier to allow users to manipulate items on a canvas.
* @author Pamblam
* @website
* @license MIT
*/
/**
* Interface for handling all canvas functionality
* @see https://pamblam.github.io/canvas-layers/examples/
* @version 2.1.4
*/
class Canvas{
/**
* Construct a new instance of the Canvas class
* @param {HTMLElement} canvas - The canvas to instantiate the class upon.
* @param {Number} [opts.anchorRadius=Canvas.anchorRadius] - The radius of the anchor points shown on selected elements.
* @param {String} [opts.strokeStyle=Canvas.strokeStyle] - The color of the outlines drawn on selceted elements. May be any valid CSS color string.
* @param {String} [opts.fillStyle=Canvas.fillStyle] - The color of the anchor points shown on selected elements. May be any valid CSS color string.
* @param {Number} [opts.lineWidth=Canvas.lineWidth] - The width of the outlines shown on selected elements.
* @param {String} [opts.cursors.default=Canvas.cursors.default] - The default cursor to use when hovering over the canvas. May be any valid css cursor value.
* @param {String} [opts.cursors.grab=Canvas.cursors.grab] - The grab cursor to use when hovering over a movable layer. May be any valid css cursor value.
* @param {String} [opts.cursors.grabbing=Canvas.cursors.grabbing] - The grabbing cursor to use when dragging a layer. May be any valid css cursor value.
* @param {String} [opts.cursors.move=Canvas.cursors.move] - The default cursor to use when hovering over a resize anchor. May be any valid css cursor value.
* @param {String} [opts.cursors.rotate=Canvas.cursors.rotate] - The default cursor to use when hovering a rotate anchor point. May be any valid css cursor value.
* @param {String} [opts.cursors.rotating=Canvas.cursors.rotating] - The default cursor to use when rotating an active layer. May be any valid css cursor value.
* @returns {Canvas}
*/
constructor(canvas, opts={}){
this.canvas = canvas;
this.width = canvas.width;
this.height = canvas.height;
this.ctx = canvas.getContext('2d');
this.layers = [];
this.layer_state_pos = -1;
this.layer_states = [];
this.drawPromises = [];
this.activeLayer = null;
this.shiftKeyDown = false;
this.draggingActiveLayer = false;
this.resizingActiveLayer = false;
this.rotatingActiveLayer = false;
this.lastMouseDownOffset = {x:0, y:0};
this.activeLayerMouseOffset = {x:0, y:0};
this.activeLayerOriginalDimensions = {width:0, height:0};
this.activeLayerRotateStartPos = {x:0, y:0};
this.displayGrid = false;
this.snapToGrid = false;
this.gridDistancePixels = 10;
// bind handlers once and store them
this._onmousemove = this.onmousemove.bind(this);
this._onmousedown = this.onmousedown.bind(this);
this._onmousereset = this.onmousereset.bind(this);
this._onclick = this.onclick.bind(this);
this._ondblclick = this.ondblclick.bind(this);
this._onkeyevent = this.onkeyevent.bind(this);
// attach using the stored references
canvas.addEventListener('mousemove', this._onmousemove);
canvas.addEventListener('mousedown', this._onmousedown);
canvas.addEventListener('mouseout', this._onmousereset);
canvas.addEventListener('mouseup', this._onmousereset);
canvas.addEventListener('click', this._onclick);
canvas.addEventListener('dblclick', this._ondblclick);
document.addEventListener('keydown', this._onkeyevent);
document.addEventListener('keyup', this._onkeyevent);
this.anchorRadius = opts.anchorRadius || Canvas.anchorRadius;
this.strokeStyle = opts.strokeStyle || Canvas.strokeStyle;
this.fillStyle = opts.fillStyle || Canvas.fillStyle;
this.lineWidth = opts.lineWidth || Canvas.lineWidth;
this.cursors = opts.cursors || {};
this.cursors.default = this.cursors.default || Canvas.cursors.default;
this.cursors.grab = this.cursors.grab || Canvas.cursors.grab;
this.cursors.grabbing = this.cursors.grabbing || Canvas.cursors.grabbing;
this.cursors.move = this.cursors.move || Canvas.cursors.move;
this.cursors.rotate = this.cursors.rotate || Canvas.cursors.rotate;
this.cursors.rotating = this.cursors.rotating || Canvas.cursors.rotating;
this.last_clicked_layer = null;
this.pending_layers = 0;
this.ready = true;
// if turned on, no state will be saved.
this.muteStateChanges = false;
this.isCtrlPressed = false;
this.ctrlGroupLayer = new CanvasLayerGroup('ctrl-grp');
}
/**
* Is the provided layer part of the ctrl-grp
* @param {CavnasLayer} layer
* @returns {Boolean}
*/
isLayerInGroup(layer){
return !!~this.ctrlGroupLayer.layers.indexOf(layer);
}
/**
* Is the ctrl-grp on the canvas?
* @returns {Boolean}
*/
isGroupOnCanvas(){
return !!~this.layers.indexOf(this.ctrlGroupLayer)
}
/**
* Remove the ctrl-grp from the canvas
* @returns {Promise}
*/
async destroyCtrlGroup(){
this.muteStateChanges = true;
var promises = this.ctrlGroupLayer.layers.map(layer=>{
return new Promise(done=>{
this.addLayer(layer);
layer.onload(()=>done());
});
});
await Promise.all(promises);
this.ctrlGroupLayer.layers = [];
this.ctrlGroupLayer.rotation = 0;
if(this.isGroupOnCanvas()) this.removeLayer(this.ctrlGroupLayer);
this.muteStateChanges = false;
}
/**
* Load the state object
* @param {type} state
* @returns {undefined}
*/
loadState(state){
this.layers = state.map(s=>CanvasLayer.deobjectify(s));
this.draggingActiveLayer = false;
this.resizingActiveLayer = false;
this.rotatingActiveLayer = false;
this.lastMouseDownOffset = {x:0, y:0};
this.activeLayerMouseOffset = {x:0, y:0};
this.activeLayerOriginalDimensions = {width:0, height:0};
this.activeLayerRotateStartPos = {x:0, y:0};
this.draw();
}
/**
* saves the current state in the state stack
* @returns {undefined}
*/
saveState(){
if(this.muteStateChanges) return;
var state = [];
const getState = (layers) => {
layers.forEach(layer=>{
if(layer instanceof CanvasLayerGroup) getState(layer.layers);
else state.push(layer.objectify());
});
}
getState(this.layers);
this.layer_states.length = this.layer_state_pos+1;
this.layer_states.push(state);
this.layer_state_pos = this.layer_states.length-1;
}
/**
* Undo an action
* @returns {undefined}
*/
undo(){
if(this.layer_state_pos>0){
this.layer_state_pos--;
this.loadState(this.layer_states[this.layer_state_pos]);
}
}
/**
* Redo the last un-did action
* @returns {undefined}
*/
redo(){
if((this.layer_state_pos+1)<this.layer_states.length){
this.layer_state_pos++;
this.loadState(this.layer_states[this.layer_state_pos]);
}
}
/**
* Enable snap to grid
* @returns {undefined}
*/
snapOn(gridDistancePixels=10){
this.snapToGrid = true;
gridDistancePixels = +gridDistancePixels < 3 ? 3 : +gridDistancePixels;
this.gridDistancePixels = gridDistancePixels;
}
/**
* Disable snap to grid
* @returns {undefined}
*/
snapOff(){
this.snapToGrid = false;
this.draw();
}
/**
* Show the grid lines on the canvas
* @returns {undefined}
*/
showGrid(gridDistancePixels=10){
this.displayGrid = true;
gridDistancePixels = +gridDistancePixels < 3 ? 3 : +gridDistancePixels;
this.gridDistancePixels = gridDistancePixels;
this.draw();
}
/**
* Hide the grid lines on the canvas
* @returns {undefined}
*/
hideGrid(){
this.displayGrid = false;
this.draw();
}
/**
* Get a layer by it's given name.
* @param {String} name - The name of the layer.
* @returns {CanvasLayer|null}
*/
getLayerByName(name){
for(var i=this.layers.length; i--;){
if(this.layers[i].name === name) return this.layers[i];
}
return null;
}
/**
* Add a layer to the canvas.
* @param {String} url - The URI or URL of an image to draw on the canvas.
* @param {String} [opts.name="Layer n"] - The name of the layer.
* @param {Number} [opts.x=this.width/2] - The x position of the layer.
* @param {Number} [opts.y=this.height/2] - The y position of the layer.
* @param {Number} [opts.rotation=0] - The rotation of the layer, counter-clockwise, in degrees.
* @param {Boolean} [opts.draggable=true] - Can the user move this layer?
* @param {Boolean} [opts.rotateable=true] - Can the user rotate this layer?
* @param {Boolean} [opts.resizable=true] - Can the user resize this layer?
* @param {Boolean} [opts.selectable=true] - Can the user select this layer?
* @param {Number} [opts.width=null] - The width of the layer to be drawn. If not specified, defaults to the images natural width.
* @param {Number} [opts.height=null] - The height of the layer to be drawn. If not specified, defaults to the images natural height.
* @param {Boolean} [opts.forceBoundary=false] - Force the item to stay in bounds.
* @param {Boolean} [opts.allowOverlap=true] - Allow layers to overlap with this one.
* @returns {CanvasLayer} - The layer that was added.
*/
addLayer(layerOrURL, opts={}){
this.ready = false;
if(layerOrURL instanceof CanvasLayer){
var layer = layerOrURL;
}else{
const name = opts.name || `Layer ${this.layers.length}`;
const x = parseFloat(opts.x || this.width/2);
const y = parseFloat(opts.y || this.height/2);
const rotation = parseFloat(opts.rotation || 0);
const draggable = opts.draggable === undefined ? true : opts.draggable;
const rotateable = !!opts.rotateable === undefined ? true : opts.rotateable;
const resizable = !!opts.resizable === undefined ? true : opts.resizable;
const selectable = !!opts.selectable === undefined ? true : opts.selectable;
const width = opts.width || null;
const height = opts.height || null;
const forceBoundary = opts.forceBoundary || false;
const allowOverlap = opts.hasOwnProperty('allowOverlap') ? !!opts.allowOverlap : true;
var layer = new CanvasLayer(layerOrURL, name, x, y, width, height, rotation, draggable, rotateable, resizable, selectable, forceBoundary, allowOverlap);
}
this.layers.unshift(layer);
this.pending_layers++;
layer.onload(()=>{
this.pending_layers--;
if(0 === this.pending_layers){
this.ready = true;
this.draw();
this.saveState();
if(!(layer instanceof CanvasLayerGroup)){
this.fireEvent('layer-added');
}
}
});
return layer;
}
/**
* Rotate and crop the canvas to the dimensions and rotation of the specified layer.
* @param {CanvasLayer} layer - The layer to crop to.
* @returns {Promise} - A Promise htat resolves with the DataURI of the cropped area.
*/
cropToLayer(layer, unrotated=true){
return this.extractPortion(layer.x, layer.y, layer.width, layer.height, layer.rotation, unrotated);
}
/**
* Rotate and extract a custom area of the canvas.
* @param {Number} centerx - The x position of the center of the area to extract.
* @param {Number} centery - The y position of the center of the area to extract.
* @param {Number} width - The width of the area to extract from teh canvas.
* @param {Number} height - The height of the area to extract from teh canvas.
* @param {Number} [rotation=0] - The rotation of the area to extract, counter-clockwise, in degrees.
* @param {Boolean} [unrotated=true] - If true, undo the rotation so the layer is in it's natural position.
* @returns {Promise} - A Promise htat resolves with the DataURI of the cropped area.
*/
async extractPortion(centerx, centery, width, height, rotation=0, unrotated=true){
var radians = rotation * Math.PI / 180;
var {x, y} = Canvas.absolutePoint(-(width/2), -(height/2), centerx, centery, rotation);
var rectBB = this.getRotatedRectBB(x, y, width, height, radians);
var canvas0 = document.createElement("canvas");
var ctx0 = canvas0.getContext("2d");
var canvas1 = document.createElement("canvas");
var ctx1 = canvas1.getContext("2d");
var canvas2 = document.createElement("canvas");
var ctx2 = canvas2.getContext("2d");
canvas1.width = canvas2.width = rectBB.width;
canvas1.height = canvas2.height = rectBB.height;
canvas0.width = this.width;
canvas0.height = this.height;
await this.loadAll();
for(let i=this.layers.length; i--;){
let layer = this.layers[i];
var radians = layer.rotation * (Math.PI/180);
ctx0.translate(layer.x, layer.y);
ctx0.rotate(radians);
ctx0.drawImage(layer.image, -(layer.width/2), -(layer.height/2), layer.width, layer.height);
ctx0.rotate(-radians);
ctx0.translate(-layer.x, -layer.y);
}
ctx1.drawImage(canvas0, rectBB.cx - rectBB.width / 2, rectBB.cy - rectBB.height / 2, rectBB.width, rectBB.height, 0, 0, rectBB.width, rectBB.height);
if(!unrotated){
return canvas1.toDataURL();
}
ctx2.translate(canvas1.width / 2, canvas1.height / 2);
ctx2.rotate(-radians);
ctx2.drawImage(canvas1, -canvas1.width / 2, -canvas1.height / 2);
var ofstx = (canvas2.width - width) / 2;
var ofsty = (canvas2.height - height) / 2;
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
canvas1.width = width;
canvas1.height = height;
ctx1.drawImage(canvas2, -ofstx, -ofsty);
return canvas1.toDataURL();
}
/**
* Draw the canvas.
* @returns {Promise}
*/
draw(){
return new Promise(done=>{
this.drawPromises.push(done);
if(!this.ready) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for(let i=this.layers.length; i--;){
let layer = this.layers[i];
var radians = layer.rotation * (Math.PI/180);
this.ctx.translate(layer.x, layer.y);
this.ctx.rotate(radians);
this.ctx.drawImage(layer.image, -(layer.width/2), -(layer.height/2), layer.width, layer.height);
if(layer === this.activeLayer){
this.ctx.strokeStyle = this.strokeStyle;
this.ctx.fillStyle = this.fillStyle;
this.ctx.lineWidth = this.getScale() * this.lineWidth;
this.ctx.strokeRect(-(layer.width/2), -(layer.height/2), layer.width, layer.height);
if(layer.resizable){
layer.getCorners().forEach(corner=>{
this.drawCircle(corner.x, corner.y, this.getScale() * this.anchorRadius);
});
}
if(layer.rotateable){
this.ctx.beginPath();
this.ctx.moveTo(0, 0);
this.ctx.lineTo((layer.width/2)+25, 0);
this.ctx.stroke();
this.drawCircle((layer.width/2)+25, 0, this.getScale() * this.anchorRadius);
}
}
this.ctx.rotate(-radians);
this.ctx.translate(-layer.x, -layer.y);
}
if(this.displayGrid){
this.ctx.strokeStyle = "rgba(0,0,0,0.2)";
this.ctx.lineWidth = this.getScale() * 2;
var {xs, ys} = this.getGridLines(false);
xs.forEach(x=>{
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.canvas.height);
this.ctx.stroke();
});
ys.forEach(y=>{
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.canvas.width, y);
this.ctx.stroke();
});
}
while(this.drawPromises.length) this.drawPromises.shift()();
this.canvas.dispatchEvent(new CustomEvent('canvas-drawn'));
});
}
/**
* Remove all layers from teh canvas.
* @returns {undefined}
*/
removeAllLayers(){
this.deSelectLayer();
this.layers = [];
this.draw();
}
/**
* Destroy the whole thing
*/
destroy() {
// remove event listeners
if (this.canvas) {
this.canvas.removeEventListener('mousemove', this._onmousemove);
this.canvas.removeEventListener('mousedown', this._onmousedown);
this.canvas.removeEventListener('mouseout', this._onmousereset);
this.canvas.removeEventListener('mouseup', this._onmousereset);
this.canvas.removeEventListener('click', this._onclick);
this.canvas.removeEventListener('dblclick', this._ondblclick);
}
document.removeEventListener('keydown', this._onkeyevent);
document.removeEventListener('keyup', this._onkeyevent);
// clear canvas contents
if (this.ctx && this.canvas) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
// break references to help GC
this.removeAllLayers();
this.layers = [];
this.layer_states = [];
this.activeLayer = null;
this.ctrlGroupLayer = null;
this.canvas = null;
this.ctx = null;
// optional: flip a flag so methods early-return
this.ready = false;
}
/**
* Remove the specified layer from the canvas.
* @param {CanvasLayer} layer - The layer to remove
* @returns {undefined}
*/
removeLayer(layer){
if(layer === this.activeLayer) this.deSelectLayer();
this.layers.splice(this.layers.indexOf(layer), 1);
this.saveState();
this.draw();
}
/**
* Select the given layer.
* @param {CanvasLayer} layer - The layer to select.
* @returns {undefined}
*/
selectLayer(layer){
this.layers.unshift(this.layers.splice(this.layers.indexOf(layer), 1)[0]);
this.activeLayer = layer;
this.draw();
this.fireEvent('layer-selected');
}
/**
* Deselect the selected layer if one is selected.
* @returns {undefined}
*/
deSelectLayer(){
this.activeLayer = null;
this.draggingActiveLayer = false;
this.draw();
this.fireEvent('layer-deselected');
}
/**
* Get the cooresponding coordinates of the mouses position on the canvas.
* @param {MouseEvent} e - The event passed to a mouse event handler.
* @returns {{x: Number, y: Number}}
*/
canvasMousePos(e) {
var rect = this.canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
var wfactor = this.canvas.width / rect.width;
var hfactor = this.canvas.height / rect.height;
x = x*wfactor;
y = y*hfactor;
return {x, y};
}
/**
* Get the layer at the given canvas coordinates.
* @param {Number} x - The x ordinate.
* @param {Number} y - The y ordinate.
* @returns {CanvasLayer|null}
*/
getLayerAt(x, y){
for(let i=0; i<this.layers.length; i++){
let layer = this.layers[i];
if(Canvas.isOverLayer(x, y, layer)) return layer;
}
return null;
}
/**
* Are the given coordinates over a selectable layer?
* @param {Number} x - The x ordinate.
* @param {Number} y - The y ordinate.
* @returns {Boolean}
*/
isOverSelectableLayer(x, y){
for(let i=this.layers.length; i--;){
if(Canvas.isOverLayer(x, y, this.layers[i])){
if(this.layers[i].selectable && this.activeLayer !== this.layers[i]) return true;
}
}
return false;
}
/**
* Get an array of all layers that the given layer overlaps.
* @param {type} layer
* @returns {Array|Canvas.getOverlappingLayers.layers}
*/
getOverlappingLayers(layer){
var layers = [];
for(var i=0; i<this.layers.length; i++){
if(this.layers[i] === layer) continue;
if(this.doLayersOverlap(layer, this.layers[i])){
layers.push(this.layers[i]);
}
}
return layers;
}
////////////////////////////////////////////////////////////////////////////
// Undocumented utility layers /////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Get an object containing arrays of x and y grid line positons
* @param {Boolean} snap - If true, get lines to snap to, else get lines to display
* @returns {Object} - An object with an 'xs' property containing x positions and a 'ys' property containing y positions;
*/
getGridLines(snap=true){
var xs = [];
var ys = [];
var dist = 0;
if(snap){
dist = this.gridDistancePixels;
}else{
dist = this.gridDistancePixels * 2;
}
for(var x=0; x<this.canvas.width; x += (this.getScale() * dist)){
xs.push(x);
}
for(var y=0; y<this.canvas.height; y += (this.getScale() * dist)){
ys.push(y);
}
for(let i=this.layers.length; i--;){
if(snap && this.layers[i] === this.activeLayer) continue;
xs.push(this.layers[i].x);
ys.push(this.layers[i].y);
}
[...new Set(xs)].sort();
[...new Set(ys)].sort();
return {xs, ys};
}
/**
* Load all layers.
* @ignore
*/
loadAll(){
var promises = this.layers.map(layer=>layer.load());
return Promise.all(promises);
}
/**
* Get the bounding box of the defined area.
* @ignore
*/
getRotatedRectBB(x, y, width, height, rAngle) {
var absCos = Math.abs(Math.cos(rAngle));
var absSin = Math.abs(Math.sin(rAngle));
var cx = x + width / 2 * Math.cos(rAngle) - height / 2 * Math.sin(rAngle);
var cy = y + width / 2 * Math.sin(rAngle) + height / 2 * Math.cos(rAngle);
var w = width * absCos + height * absSin;
var h = width * absSin + height * absCos;
return ({
cx: cx,
cy: cy,
width: w,
height: h
});
}
/**
* Draw a circle on the canvas.
* @ignore
*/
drawCircle(x, y, radius){
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI*2, true);
this.ctx.closePath();
this.ctx.fill();
}
/**
* Handle key down and keyup.
* @ignore
*/
onkeyevent(e){
if('Shift' === e.key){
if(e.type === 'keydown') this.shiftKeyDown = true;
else this.shiftKeyDown = false
}
if('Control' === e.key){
if(e.type === 'keydown') this.isCtrlPressed = true;
else this.isCtrlPressed = false
}
if(!this.isCtrlPressed && this.isGroupOnCanvas()){
this.destroyCtrlGroup();
}
}
/**
* Check if two given layers overlap
* @param {CanvasLayer} layer1
* @param {CanvasLayer} layer2
* @returns {Boolean}
*/
doLayersOverlap(layer1, layer2){
const abs_corners = l => l.getCorners().map(c=>Canvas.absolutePoint(c.x, c.y, l.x, l.y, l.rotation));
const corners_to_lines = c => [
[{x:c[0].x, y:c[0].y},{x:c[1].x, y:c[1].y}],
[{x:c[1].x, y:c[1].y},{x:c[2].x, y:c[2].y}],
[{x:c[2].x, y:c[2].y},{x:c[3].x, y:c[3].y}],
[{x:c[3].x, y:c[3].y},{x:c[0].x, y:c[0].y}]
];
var l1_corners = abs_corners(layer1);
var l1_lines = corners_to_lines(l1_corners);
var l2_corners = abs_corners(layer2);
var l2_lines = corners_to_lines(l2_corners);
// Check if any of the edges intersect
// This covers partial overlaps.
for(let n1=0; n1<l1_lines.length; n1++){
for(let n2=0; n2<l2_lines.length; n2++){
let a = l1_lines[n1][0].x;
let b = l1_lines[n1][0].y;
let c = l1_lines[n1][1].x;
let d = l1_lines[n1][1].y;
let p = l2_lines[n2][0].x;
let q = l2_lines[n2][0].y;
let r = l2_lines[n2][1].x;
let s = l2_lines[n2][1].y;
if(Canvas.doLinesIntersect(a, b, c, d, p, q, r, s)) return true;
}
}
// Check for one corner. This covers full overlaps.
var c1 = layer1.getCorners()[0];
c1 = Canvas.absolutePoint(c1.x, c1.y, layer1.x, layer1.y, layer1.rotation);
if(Canvas.isOverLayer(c1.x, c1.y, layer2)) return true;
var c2 = layer2.getCorners()[0];
c2 = Canvas.absolutePoint(c2.x, c2.y, layer2.x, layer2.y, layer2.rotation);
if(Canvas.isOverLayer(c2.x, c2.y, layer1)) return true;
return false;
}
/**
* Returns true if the active layer can be moved to the specified coordinates.
* @ignore
*/
canMoveActiveLayer(newx, newy){
const inBounds = this.isNewPosInBounds(this.activeLayer, newx, newy, this.activeLayer.width, this.activeLayer.height);
if(this.activeLayer.forceBoundary && !inBounds) return false;
var x = this.activeLayer.x;
var y = this.activeLayer.y;
this.activeLayer.x = newx;
this.activeLayer.y = newy;
var canMove = true;
for(var i=0; i<this.layers.length; i++){
if(this.layers[i] === this.activeLayer) continue;
if(this.activeLayer.allowOverlap && this.layers[i].allowOverlap) continue;
if(this.doLayersOverlap(this.activeLayer, this.layers[i])){
canMove = false;
break;
}
}
this.activeLayer.x = x;
this.activeLayer.y = y;
return canMove;
}
/**
* Returns true if the active layer can be resized to the specified dimensions.
* @ignore
*/
canResizeActiveLayer(width, height){
const inBounds = this.isNewPosInBounds(this.activeLayer, this.activeLayer.x, this.activeLayer.y, width, height);
if(this.activeLayer.forceBoundary && !inBounds) return false;
var w = this.activeLayer.width;
var h = this.activeLayer.height;
this.activeLayer.width = w;
this.activeLayer.height = h;
var canResize = true;
for(var i=0; i<this.layers.length; i++){
if(this.layers[i] === this.activeLayer) continue;
if(this.activeLayer.allowOverlap && this.layers[i].allowOverlap) continue;
if(this.doLayersOverlap(this.activeLayer, this.layers[i])){
canResize = false;
break;
}
}
this.activeLayer.width = w;
this.activeLayer.height = h;
return canResize;
}
/**
* Returns true if the active layer can be rotated to the specified degree.
* @ignore
*/
canRotateActiveLayer(degrees){
var r = this.activeLayer.rotation;
this.activeLayer.rotation = degrees;
const inBounds = this.isNewPosInBounds(this.activeLayer, this.activeLayer.x, this.activeLayer.y, this.activeLayer.width, this.activeLayer.height);
if(this.activeLayer.forceBoundary && !inBounds) return false;
var canRotate = true;
for(var i=0; i<this.layers.length; i++){
if(this.layers[i] === this.activeLayer) continue;
if(this.activeLayer.allowOverlap && this.layers[i].allowOverlap) continue;
if(this.doLayersOverlap(this.activeLayer, this.layers[i])){
canRotate = false;
break;
}
}
this.activeLayer.rotation = r;
return canRotate;
}
/**
* Handle mouse moves over the canvas.
* @ignore
*/
onmousemove(e){
var {x, y} = this.canvasMousePos(e);
this.setCursor(x, y);
if(this.activeLayer === null) return;
if(this.rotatingActiveLayer){
var dx = x - this.activeLayer.x;
var dy = y - this.activeLayer.y;
var angle = Math.atan2(dy, dx);
var degrees = angle * 180 / Math.PI;
if(!this.canRotateActiveLayer(degrees)){
this.rotatingActiveLayer = false;
this.draw();
return;
}
if(this.fireEvent('layer-rotate')){
this.activeLayer.rotation = degrees;
if(this.activeLayer instanceof CanvasLayerGroup){
this.activeLayer.updateLayers();
}
this.draw();
}
}else if(this.draggingActiveLayer){
const newx = this.activeLayerMouseOffset.x + x;
const newy = this.activeLayerMouseOffset.y + y;
if(!this.canMoveActiveLayer(newx, newy)){
this.draggingActiveLayer = false;
this.draw();
return;
}
if(this.fireEvent('layer-drag')){
var moveRightPixels = newx - this.activeLayer.x;
var moveDownPixels = newy - this.activeLayer.y;
this.activeLayer.x += moveRightPixels;
this.activeLayer.y += moveDownPixels;
if(this.activeLayer instanceof CanvasLayerGroup){
this.activeLayer.updateLayers();
}
this.draw();
}
}else if(this.resizingActiveLayer){
const {width, height} = this.calculateLayerResize(x, y);
if(!this.canResizeActiveLayer(width, height)){
this.draggingActiveLayer = false;
this.draw();
return;
}
if(this.fireEvent('layer-resize')){
this.activeLayer.width = width;
this.activeLayer.height = height;
if(this.activeLayer instanceof CanvasLayerGroup){
this.activeLayer.updateLayers();
}
this.draw();
}
}
}
/**
* Set the appropriate cursor.
* @ignore
*/
setCursor(x, y){
if(this.rotatingActiveLayer){
document.body.style.cursor = this.cursors.rotating;
}else if(this.draggingActiveLayer){
document.body.style.cursor = this.cursors.grabbing;
}else if(this.resizingActiveLayer){
document.body.style.cursor = this.cursors.move;
}else if(this.isNearActiveCorner(x, y)){
document.body.style.cursor = this.cursors.move;
}else if(this.isNearActiveRotatePoint(x, y)){
document.body.style.cursor = this.cursors.rotate;
}else if(this.isOverSelectableLayer(x, y)){
document.body.style.cursor = this.cursors.grab;
}else{
document.body.style.cursor = this.cursors.default;
}
}
/**
* Calculate new width and height of resizing image
* @ignore
*/
calculateLayerResize(x, y){
var width = this.activeLayer.width;
var height = this.activeLayer.height;
var o = this.lastMouseDownOffset;
var n = Canvas.layerRelativePoint(x, y, this.activeLayer);
if(o.x > 0){
width = Math.abs(this.activeLayerOriginalDimensions.width - (o.x-n.x)*2);
}else{
width = Math.abs(this.activeLayerOriginalDimensions.width - (n.x-o.x)*2);
}
if(o.y > 0){
height = Math.abs(this.activeLayerOriginalDimensions.height - (o.y-n.y)*2);
}else{
height = Math.abs(this.activeLayerOriginalDimensions.height - (n.y-o.y)*2);
}
if(this.shiftKeyDown){
var ratio = Math.min(
width/this.activeLayerOriginalDimensions.width,
height/this.activeLayerOriginalDimensions.height
);
width = this.activeLayerOriginalDimensions.width * ratio;
height = this.activeLayerOriginalDimensions.height * ratio;
}
return {width, height};
}
/**
* Fire an event.
* @ignore
*/
fireEvent(type){
var event = new CustomEvent(type, {detail: this, cancelable: true, bubbles: true});
return this.canvas.dispatchEvent(event);
}
/**
* Listen for click event on a layer
* @ignore
*/
onclick(e){
var {x, y} = this.canvasMousePos(e);
var lcl = this.getLayerAt(x, y);
if(lcl){
this.last_clicked_layer = lcl;
this.fireEvent('layer-click');
}
}
/**
* Listen for dbl click event on a layer
* @ignore
*/
ondblclick(e){
var {x, y} = this.canvasMousePos(e);
var lcl = this.getLayerAt(x, y);
if(lcl){
this.last_clicked_layer = lcl;
this.fireEvent('layer-dblclick');
}
}
/**
* Handle mousedown over the canvas.
* @ignore
*/
async onmousedown(e){
var {x, y} = this.canvasMousePos(e);
this.setCursor(x, y);
if(this.isNearActiveRotatePoint(x, y)){
if(this.fireEvent('layer-rotate-start')){
this.activeLayerRotateStartPos = {x, y};
this.rotatingActiveLayer = true;
}
}else if(this.isNearActiveCorner(x, y)){
if(this.fireEvent('layer-resize-start')){
this.resizingActiveLayer = true;
}
}else{
var cancelled = false;
var layer = this.getLayerAt(x, y);
if(layer !== null && layer.selectable === false) layer = null;
if(layer !== null && this.activeLayer !== null && layer !== this.activeLayer){
cancelled = !this.fireEvent('layer-deselect');
if(!cancelled) !this.deSelectLayer();
}
if(!cancelled && layer !== null && this.fireEvent('layer-drag-start')){
this.activeLayerMouseOffset.x = layer.x - x;
this.activeLayerMouseOffset.y = layer.y - y;
if(layer.draggable) this.draggingActiveLayer = true;
if(layer !== this.activeLayer){
if(this.fireEvent('layer-select')){
this.selectLayer(layer);
}
}
}
}
if(this.activeLayer){
this.activeLayerOriginalDimensions = {
width: this.activeLayer.width,
height: this.activeLayer.height
};
this.lastMouseDownOffset = Canvas.layerRelativePoint(x, y, this.activeLayer);
}
// Handling the grouping
var layer = this.getLayerAt(x, y);
if(layer && layer !== this.ctrlGroupLayer && layer.selectable){
if(!this.isCtrlPressed) this.destroyCtrlGroup();
if(!this.isLayerInGroup(layer)){
this.muteStateChanges = true;
await this.ctrlGroupLayer.addLayer(layer);
if(this.ctrlGroupLayer.layers.length === 1) this.selectLayer(layer);
layer.onload(()=>{
this.muteStateChanges = false;
});
}
if(!this.isGroupOnCanvas() && this.ctrlGroupLayer.layers.length > 1){
this.muteStateChanges = true;
this.ctrlGroupLayer.layers.forEach(l=>{
this.removeLayer(l);
});
this.addLayer(this.ctrlGroupLayer);
this.ctrlGroupLayer.onload(()=>{
this.muteStateChanges = false;
});
}
if(this.isGroupOnCanvas()){
this.selectLayer(this.ctrlGroupLayer);
}
}
}
/**
* Are teh given coordinates near an active rotate anchor.
* @ignore
*/
isNearActiveRotatePoint(x, y){
if(!this.activeLayer || !this.activeLayer.rotateable) return false;
var {x, y} = Canvas.layerRelativePoint(x, y, this.activeLayer);
var mx = (this.activeLayer.width/2)+25;
var my = 0;
var dist = Math.hypot(mx-x, my-y);
if(dist <= this.getScale() * this.anchorRadius) return true;
return false;
}
/**
* Are the given coordinates near an active resize anchor.
* @ignore
*/
isNearActiveCorner(x, y){
if(!this.activeLayer || !this.activeLayer.resizable) return false;
var {x, y} = Canvas.layerRelativePoint(x, y, this.activeLayer);
var isNear = false;
this.activeLayer.getCorners().forEach(corner=>{
var dist = Math.hypot(corner.x-x, corner.y-y);
if(dist <= this.getScale() * this.anchorRadius) isNear = true;
});
return isNear;
}
/**
* Given a position, check if it is in bounds
* @ignore
*/
isNewPosInBounds(layer, x, y, width, height){
var _x = layer.x;
var _y = layer.y;
var _width = layer.width;
var _height = layer.height;
layer.x = x;
layer.y = y;
layer.width = width;
layer.height = height;
var inbounds = true;
layer.getCorners().forEach(corner => {
var pos = Canvas.absolutePoint(corner.x, corner.y, layer.x, layer.y, layer.rotation);
if (pos.x < 0 || pos.x > this.width || pos.y < 0 || pos.y > this.width) {
inbounds = false;
}
});
layer.x = _x;
layer.y = _y;
layer.width = _width;
layer.height = _height;
return inbounds;
}
/**
* Get nearest grid line
* @ignore
*/
getNearestGridline(n, grid){
return Object.values(grid.reduce((acc, val)=>{
if(val > n){
if(null === acc.high) acc.high = val;
else acc.high = Math.min(acc.high, val);
}else{
if(null === acc.low) acc.low = val;
else acc.low = Math.max(acc.low, val);
}
return acc;
}, {low: null, high: null})).reduce((acc, val)=>{
var valDistToN = Math.abs(val - n);
var accDistToN = acc === null ? null : Math.abs(acc - n);
if(acc === null || valDistToN < accDistToN) acc = val;
return acc;
}, null);
}
/**
* Handle mouseup or mouseout.
* @ignore
*/
onmousereset(e){
if(this.draggingActiveLayer) this.fireEvent("layer-drag-end");
if(this.resizingActiveLayer) this.fireEvent("layer-resize-end");
if(this.rotatingActiveLayer) this.fireEvent("layer-rotate-end");
if(this.draggingActiveLayer && this.snapToGrid && this.activeLayer){
var {xs, ys} = this.getGridLines();
var closestx = this.getNearestGridline(this.activeLayer.x, xs);
var closesty = this.getNearestGridline(this.activeLayer.y, ys);
var redraw_required = false;
var dist = Math.abs(closestx - this.activeLayer.x);
if(dist <= this.gridDistancePixels && dist !== 0){
this.activeLayer.x = closestx;
redraw_required = true;
}
dist = Math.abs(closesty - this.activeLayer.y);
if(dist <= this.gridDistancePixels && dist !== 0){
this.activeLayer.y = closesty;
redraw_required = true;
}
if(redraw_required){
this.draw();
}
}
if(this.draggingActiveLayer || this.resizingActiveLayer || this.rotatingActiveLayer) this.saveState();
var {x, y} = this.canvasMousePos(e);
this.draggingActiveLayer = false;
this.resizingActiveLayer = false;
this.rotatingActiveLayer = false;
this.lastMouseDownOffset = {x:0, y:0};
this.activeLayerMouseOffset = {x:0, y:0};
this.activeLayerOriginalDimensions = {width:0, height:0};
this.activeLayerRotateStartPos = {x:0, y:0};
this.setCursor(x, y);
}
/**
* Get the scale of the canvas
* @ignore
*/
getScale(){
var rect = this.canvas.getBoundingClientRect();
return this.canvas.width / rect.width;
}
}
/**
* The version of the library
* @type {String}
*/
Canvas.version = '2.1.4';
/**
* The default anchorRadius value for all Canvas instances.
* @type {Number}
*/
Canvas.anchorRadius = 8;
/**
* The default strokeStyle value for all Canvas instances.
* @type {String}
*/
Canvas.strokeStyle = '#ba0000';
/**
* The default fillStyle value for all Canvas instances.
* @type {String}
*/
Canvas.fillStyle = 'black';
/**
* The default lineWidth value for all Canvas instances.
* @type {Number}
*/
Canvas.lineWidth = 5;
/**
* The default Cursor values for all Canvas instances. See the canvas constructor for details.
* @type {Object}
* @property {String} Canvas.cursors.default
* @property {String} Canvas.cursors.grab
* @property {String} Canvas.cursors.grabbing
* @property {String} Canvas.cursors.move
* @property {String} Canvas.cursors.rotate
* @property {String} Canvas.cursors.rotating
*/
Canvas.cursors = {
default: null,
grab: "grab",
grabbing: "grabbing",
move: "crosshair",
rotate: "grab",
rotating: "grabbing"
};
/**
* Convert a relative point to an absolute point.
* @ignore
*/
Canvas.absolutePoint = (relPointX, relPointY, centerX, centerY, rotationDegrees) => {
var radians = rotationDegrees * (Math.PI / 180);
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var x = centerX + (relPointX * cos) - (relPointY * sin);
var y = centerY + (relPointX * sin) + (relPointY * cos);
return {x, y};
};
/**
* Get the position of a point relative to another point and possibly rotated.
* @ignore
*/
Canvas.relativePoint = (absPointX, absPointY, centerX, centerY, rotation) => {
absPointX -= centerX;
absPointY -= centerY;
var radians = rotation * (Math.PI / 180);
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var x = (absPointX * cos) + (absPointY * sin);
var y = (-absPointX * sin) + (absPointY * cos);
x = Math.floor(x * 100) / 100;
y = Math.floor(y * 100) / 100;
return {x, y};
};
/**
* Get the point relative to the center of a given layer.
* @ignore
*/
Canvas.layerRelativePoint = (absPointX, absPointY, layer) => {
return Canvas.relativePoint(absPointX, absPointY, layer.x, layer.y, layer.rotation);
};
/**
* Are the given coordinates over the given layer?
* @param {Number} x - The x ordinate.
* @param {Number} y - The y ordinate.
* @param {CanvasLayer} layer - The layer to check.
* @returns {Boolean}
*/
Canvas.isOverLayer = (x, y, layer) => {
let r = Canvas.layerRelativePoint(x, y, layer);
if(r.x > (layer.width/2)) return false;
if(r.x < -(layer.width/2)) return false;
if(r.y > (layer.height/2)) return false;
if(r.y < -(layer.height/2)) return false;
return true;
};
/**
* returns true if the line from (a,b)->(c,d) intersects with (p,q)->(r,s)
* @url https://stackoverflow.com/questions/9043805/test-if-two-lines-intersect-javascript-function
* @returns {Boolean}
*/
Canvas.doLinesIntersect = (a,b,c,d,p,q,r,s) => {
var det, gamma, lambda;
det = (c - a) * (s - q) - (r - p) * (d - b);
if (det === 0) {
return false;
} else {
lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
}
};
/**
* Class representing the layers drawn on the canvas.
*/
class CanvasLayer{
/**
* Create a new Layer.
* @param {String} url - The URL or URI of an image to draw on the canvas.
* @param {String} name - The name of the layer.
* @param {Number} x - The x position of the layer on the canvas.
* @param {Number} y - The y position of the layer on the canvas.
* @param {Number} [width=null] - The width of the layer on the canvas.
* @param {Number} [height=null] - The height of the layer on the canvas.
* @param {Number} [rotation=0] - The rotation of the layer on the canvas.
* @param {Boolean} [draggable=true] - Is the layer draggable?
* @param {Boolean} [rotateable=true] - Is the layer rotateable?
* @param {Boolean} [resizable=true] - Is the layer resizable?
* @param {Boolean} [selectable=true] - Is the layer selectable?
* @param {Boolean} [forceBoundary=false] - Force the layer to stay in bounds?
* @param {Boolean} [opts.allowOverlap=true] - Allow layers to overlap with this one.
* @returns {CanvasLayer}
*/
constructor(url, name, x, y, width=null, height=null, rotation=0, draggable=true, rotateable=true, resizable=true, selectable=true, forceBoundary=false, allowOverlap=true){
this.name = name;
this.url = url;
this.ready = false;
this.image = null;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rotation = rotation;
this.draggable = draggable;
this.rotateable = rotateable;
this.resizable = resizable;
this.selectable = selectable;
this.forceBoundary = forceBoundary;
this.allowOverlap = allowOverlap;
this.load_cb_stack = [];
this.xoffset = 0;
this.yoffset = 0;
this.roffset = 0;
this.owidth = 0;
this.oheight = 0;
this.load();
}
/**
* jsonify the current layer
* @returns {String} - Serialized layer
*/
objectify(){
return {
layer: this,
state: {
name: this.name,
url: this.url,
x: this.x,
y: this.y,
width: this.width,
height: this.height,
rotation: this.rotation,
draggable: this.draggable,
rotatable: this.rotateable,
resizable: this.resizable,
selectable: this.selectable,
forceBoundary: this.forceBoundary
}
};
}
/**
* Register a function to be called when the layer is fully loaded.
* @param {Function} fn - The callback function.
* @returns {undefined}
*/
onload(fn){
if(this.ready){
fn();
return;
}else{
this.load_cb_stack.push(fn);
}
}
/**
* Load the layer so it is ready to use.
* @returns {Promise} - A promise that resolves when the layer is ready
*/
load(){
return new Promise(done=>{
if(this.ready){
done();
}else{
const img = new Image();
img.onload = ()=>{
this.image = img;
if(this.width===null) this.width = img.width;
if(this.height===null) this.height = img.height;
this.ready = true;
this.load_cb_stack.forEach(fn=>fn());
this.load_cb_stack = [];
done();
};
img.src = this.url;
}
});
}
/**
* Get the relative position of all the corners.
* @ignore
*/
getCorners(){
return [
{x:-(this.width/2), y:-(this.height/2)},
{x:-(this.width/2)+this.width, y:-(this.height/2)},
{x:-(this.width/2)+this.width, y:-(this.height/2)+this.height},
{x:-(this.width/2), y:-(this.height/2)+this.height}
];
}
}
/**
* un Serialize a layer
* @param {type} str
* @returns {CanvasLayer}
*/
CanvasLayer.deobjectify = function(d){
var layer = d.layer;
Object.keys(d.state).forEach(key=>{
layer[key] = d.state[key];
});
return layer;
};
/**
* CavnasLayer that controls multiple layers
*/
class CanvasLayerGroup extends CanvasLayer{
/**
* Create a new Layer.
* @param {String} name - The name of the layer.
* @param {Boolean} [draggable=true] - Is the layer draggable?
* @param {Boolean} [rotateable=true] - Is the layer rotateable?
* @param {Boolean} [resizable=true] - Is the layer resizable?
* @param {Boolean} [selectable=true] - Is the layer selectable?
* @param {Boolean} [forceBoundary=false] - Force the layer to stay in bounds?
* @returns {CanvasLayerGroup}
*/
constructor(name, draggable=true, rotateable=true, resizable=true, selectable=true, forceBoundary=false){
var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/1+yHgAHtAKYD9BncgAAAABJRU5ErkJggg==';
super(url, name, 0, 0, 1, 1, 0, draggable, rotateable, resizable, selectable, forceBoundary);
this.layers = [];
}
/**
* Get the layer on the given canvas at the given position. If this group
* is selected it will return the layer in this group at the given
* position, if applicatble.
* @param {Canvas} canvas - The Canvas element that owns the layers.
* @param {Number} x - The x position of the mouseclick relative to the canvas.
* @param {Number} y - The y position of the mouseclick relative to the canvas.
* @returns {layer|null}
*/
getLayerOrSubLayerAt(canvas, x, y){
for(let i=0; i<canvas.layers.length; i++){
let layer = canvas.layers[i];
if(layer === this){
for(let i=this.layers.length; i--;){
let layer = this.layers[i];
if(Canvas.isOverLayer(x, y, layer)) return layer;
}
}
if(Canvas.isOverLayer(x, y, layer)) return layer;
}
return null;
}
/**
* Remove the provided layer from the group.
* @param {CanvasLayer} layer - The layer to remove.
* @returns {Promise}
*/
async removeLayer(layer){
delete layer.xoffset;
delete layer.yoffset;
this.layers.splice(this.layers.indexOf(layer), 1);
return await this.regenerate();
}
/**
* Add a layer to the group
* @param {CanvasLayer} layer - The layer to add.
* @returns {Promise}
*/
async addLayer(layer){
if(layer === this) return;
if(layer instanceof CanvasLayerGroup){
this.layers.push(...layer.layers);
}else{
this.layers.push(layer);
}
return await this.regenerate();
}
/**
* Regenerate images and dimensions.
* @ignore
*/
async regenerate(){
var params = await this.getParams();
this.width = this.owidth = params.width;
this.height = this.oheight = params.height;
this.x = params.x;
this.y = params.y;
this.rotation = 0;
this.forceBoundary = params.forceBoundary;
this.draggable = params.draggable;
this.rotateable = params.rotateable;
this.resizable = params.resizable;
this.selectable = params.selectable;
this.url = params.uri;
this.ready = false;
return await this.load();
}
/**
* Update the sublayers of this group.
* @ignore
*/
updateLayers(){
var ratiox = this.width/this.owidth;
var ratioy = this.height/this.oheight;
this.layers.forEach(layer=>{
layer.width = layer.owidth * ratiox;
layer.height = layer.oheight * ratioy;
layer.rotation = layer.roffset + this.rotation;
var pos = Canvas.absolutePoint(layer.xoffset*ratiox, layer.yoffset*ratioy, this.x, this.y, this.rotation);
layer.x = pos.x;
layer.y = pos.y;
});
}
/**
* Regenerate images and dimensions.
* @ignore
*/
async getParams(){
const allCorners = this.layers.map(layer => {
return layer.getCorners().map(corner=>{
return Canvas.absolutePoint(corner.x, corner.y, layer.x, layer.y, layer.rotation);
});
});
const allBounds = [];
allCorners.forEach(corners=>{
allBounds.push(...corners);
});
var pos = {
left: allBounds.reduce((acc, cur)=>Math.min(acc, cur.x), Infinity),
top: allBounds.reduce((acc, cur)=>Math.min(acc, cur.y), Infinity),
right: allBounds.reduce((acc, cur)=>Math.max(acc, cur.x),0),
bottom: allBounds.reduce((acc, cur)=>Math.max(acc, cur.y),0)
};
pos.width = pos.right - pos.left;
pos.height = pos.bottom - pos.top;
pos.x = pos.left+(pos.width/2);
pos.y = pos.top+(pos.height/2);
var ele = document.createElement('canvas');
ele.width = pos.right+2;
ele.height = pos.bottom+2;
var canvas = new Canvas(ele);
this.layers.forEach(layer=>canvas.addLayer(layer));
pos.uri = await canvas.extractPortion(pos.x, pos.y, pos.width, pos.height, 0, false);
canvas.destroy();
pos.forceBoundary = this.layers.reduce((acc, itm)=>itm.forceBoundary||acc,false);
pos.draggable = this.layers.reduce((acc, itm)=>acc===false?false:itm.draggable,true);
pos.rotateable = this.layers.reduce((acc, itm)=>acc===false?false:itm.draggable,true);
pos.resizable = this.layers.reduce((acc, itm)=>acc===false?false:itm.draggable,true);
pos.selectable = this.layers.reduce((acc, itm)=>acc===false?false:itm.draggable,true);
this.layers.forEach(l=>{
l.xoffset = l.x - pos.x;
l.yoffset = l.y - pos.y;
l.roffset = l.rotation;
l.owidth = l.width;
l.oheight = l.height;
});
return pos;
}
}
/**
* Extention class that provides drawing abilities
*/
class DrawingCanvas extends Canvas{
/**
* Construct a new instance of the Canvas class
* @param {HTMLElement} canvas - The canvas to instantiate the class upon.
* @param {Number} [opts.anchorRadius=Canvas.anchorRadius] - The radius of the anchor points shown on selected elements.
* @param {String} [opts.strokeStyle=Canvas.strokeStyle] - The color of the outlines drawn on selceted elements. May be any valid CSS color string.
* @param {String} [opts.fillStyle=Canvas.fillStyle] - The color of the anchor points shown on selected elements. May be any valid CSS color string.
* @param {Number} [opts.lineWidth=Canvas.lineWidth] - The width of the outlines shown on selected elements.
* @param {String} [opts.cursors.default=Canvas.cursors.default] - The default cursor to use when hovering over the canvas. May be any valid css cursor value.
* @param {String} [opts.cursors.grab=Canvas.cursors.grab] - The grab cursor to use when hovering over a movable layer. May be any valid css cursor value.
* @param {String} [opts.cursors.grabbing=Canvas.cursors.grabbing] - The grabbing cursor to use when dragging a layer. May be any valid css cursor value.
* @param {String} [opts.cursors.move=Canvas.cursors.move] - The default cursor to use when hovering over a resize anchor. May be any valid css cursor value.
* @param {String} [opts.cursors.rotate=Canvas.cursors.rotate] - The default cursor to use when hovering a rotate anchor point. May be any valid css cursor value.
* @param {String} [opts.cursors.rotating=Canvas.cursors.rotating] - The default cursor to use when rotating an active layer. May be any valid css cursor value.
* @returns {Canvas}
*/
constructor(canvas, opts={}){
super(canvas, opts);
this.drawing_mode = null;
this.line_color = '#000000';
this.fill_color = '#0000FF';
this.shape_start_pos = null;
this.is_mouse_down = false;
this.freehand_coords = [];
this.rcanvas = document.createElement('canvas');
this.rcanvas.height = this.height;
this.rcanvas.width = this.width;
this.rctx = this.rcanvas.getContext('2d');
this.ccanvas = document.createElement('canvas');
this.cctx = this.ccanvas.getContext('2d');
this.drawing_layer = null;
this.layer_dimensions = null;
}
/**
* Set the border or line width;
* @param {Number} width
* @returns {undefined}
*/
setLineWidth(width){
this.rctx.lineWidth = +width;
}
/**
* Set the CSS color style of the border or line
* @param {string} style
* @returns {undefined}
*/
setStrokeStyle(style){
this.rctx.strokeStyle = style;
}
/**
* Set the CSS color style background of the shape
* @param {string} style
* @returns {undefined}
*/
setFillStyle(style){
this.rctx.fillStyle = style;
}
////////////////////////////////////////////////////////////////////////////
// Helpers /////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* @ignore
*/
drawEllipse(ctx, x, y, w, h) {
var kappa = .5522848,
ox = (w / 2) * kappa, // control point offset horizontal
oy = (h / 2) * kappa, // control point offset vertical
xe = x + w, // x-end
ye = y + h, // y-end
xm = x + w / 2, // x-middle
ym = y + h / 2; // y-middle
ctx.save();
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.fill();
ctx.stroke();
ctx.restore();
}
/**
* @ignore
*/
renderLayer(){
const {x, y, width, height} = this.layer_dimensions;
this.ccanvas.width = width;
this.ccanvas.height = height;
this.cctx.clearRect(0, 0, width, height);
this.cctx.drawImage(this.rcanvas, x, y, width, height, 0, 0, width, height);
var duri = this.ccanvas.toDataURL();
const xpos = x + (width/2);
const ypos = y + (height/2);
if(!this.drawing_layer){
this.drawing_layer = this.addLayer(duri, {xpos, ypos});
}else{
this.drawing_layer.x = xpos;
this.drawing_layer.y = ypos;
this.drawing_layer.width = width;
this.drawing_layer.height = height;
this.drawing_layer.url = duri;
this.drawing_layer.ready = false;
this.drawing_layer.image = null;
this.drawing_layer.load().then(()=>this.draw());
}
}
/**
* @ignore
*/
recalculateLayerDimensions(newMousePos){
var x, y, width, height;
if(this.drawing_mode === 'freehand'){
this.freehand_coords.push(newMousePos);
var all_x = this.freehand_coords.map(c=>c.x);
var all_y = this.freehand_coords.map(c=>c.y);
x = Math.min(...all_x);
y = Math.min(...all_y);
var max_x = Math.max(...all_x);
var max_y = Math.max(...all_y);
width = Math.max(0.1, Math.abs(x - max_x));
height = Math.max(0.1, Math.abs(y - max_y));
}else{
x = Math.min(this.shape_start_pos.x, newMousePos.x);
y = Math.min(this.shape_start_pos.y, newMousePos.y);
width = Math.max(0.1, Math.abs(this.shape_start_pos.x - newMousePos.x));
height = Math.max(0.1, Math.abs(this.shape_start_pos.y - newMousePos.y));
}
this.layer_dimensions = {x, y, width, height};
}
/**
* @ignore
*/
onmousemove(e){
if(!this.drawing_mode) return super.onmousemove(e);
if(!this.is_mouse_down) return;
this.rctx.clearRect(0, 0, this.width, this.height);
const pos = this.canvasMousePos(e);
this.recalculateLayerDimensions(pos);
switch(this.drawing_mode){
case "rectangle":
var {x, y, width, height} = this.layer_dimensions;
this.rctx.beginPath();
this.rctx.rect(x, y, width, height);
this.rctx.fill();
this.rctx.stroke();
this.renderLayer();
break;
case "ellipse":
var {x, y, width, height} = this.layer_dimensions;
this.drawEllipse(this.rctx, x, y, width, height);
this.renderLayer();
break;
case "line":
var x1 = this.shape_start_pos.x,
y1 = this.shape_start_pos.y,
x2 = pos.x,
y2 = pos.y;
this.rctx.beginPath();
this.rctx.moveTo(x1, y1);
this.rctx.lineTo(x2, y2);
this.rctx.fill();
this.rctx.stroke();
this.renderLayer();
break;
case "freehand":
if(this.freehand_coords < 2) break;
var a = this.freehand_coords[0];
for(var i=1; i<this.freehand_coords.length; i++){
this.rctx.beginPath();
this.rctx.moveTo(a.x, a.y);
this.rctx.lineTo(this.freehand_coords[i].x, this.freehand_coords[i].y);
this.rctx.fill();
this.rctx.stroke();
a = this.freehand_coords[i];
}
this.renderLayer();
break;
}
}
/**
* @ignore
*/
onmousedown(e){
if(!this.drawing_mode) return super.onmousedown(e);
this.is_mouse_down = true;
this.shape_start_pos = this.canvasMousePos(e);
}
/**
* @ignore
*/
onmousereset(e){
if(!this.drawing_mode) return super.onmousereset(e);
this.is_mouse_down = false;
this.shape_start_pos = null;
this.drawing_layer = null;
this.layer_dimensions = null;
this.freehand_coords = [];
}
}