Source: TrackLaneScript.js

/*
This file is part of SeaSound.

SeaSound is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

SeaSound is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with SeaSound. If not, see <https://www.gnu.org/licenses/>.
*/

// TODO: Fix draw error on empty project. Empty project throws an error while doing first draw on project loading.
/**
* The track lane class is used to create and edit arrangements of tracks created by the track editor.
* This allows the construction of larger scale pieces of music with tracks in the track editor as building blocks.
* @class
* @public
*/
class TrackLaneCanvas
{
	/**
	* The coords of the mouse.
	*/
	coord = {x:0, y:0};
	/**
	* the coords of the mouse at the start of a click.
	*/
	leftClickStart = {x:0, y:0};
	/**
	* The coords of the mouse at the release of a click.
	*/
	leftClickEnd = {x:0, y:0};
	/**
	* Tracks if mouse has been pressed or not.
	*/
	mousePressed = false;
	/**
	* The list of tracks created so far.
	* Tracks are stored as a triple containing their top left and bottom right coords as well as the name of the track.
	*/
	trackList = new Array();
	/**
	* The rectangle (track) being created this mouse click.
	*/
	workingRectangle = null;
	/**
	* Flag tracking whether the user has clicked an existing rectangle.
	*/
	existingCollision = false;
	/**
	* The index the collision occured at.
	*/
	moveIndex = -1;
	/**
	* The length of the rectangle/track to draw.
	*/
	blockSize = 1;
	/**
	* The name of the rectangle to draw.
	*/
	blockName = "EMPTY";
	/**
	* Stores the size of the rectangle font. This is initialized here but computed at reset() and on running the constructor.
	*/
	rectangleFontSize = 1;
	/**
	* The line width for rectangles and cell divisions.
	*/
	lineWidth = 0.5;
	/**
	* The width of the seek line.
	*/
	seekLineWidth = 2;
	
	/**
	* Amount translate changes by.
	*/
	translateAmt = 10;
	/**
	* Amount X scaling changes by.
	*/
	scaleAmtX = 1.15;
	/**
	* Amount Y scaling changes by.
	*/
	scaleAmtY = 1.15;

	/**
	* The position of the seek line.
	*/
	seekPos = {x:0, y:0}; 

	/**
	* The various modes a tracklane widget can be in.
	* Seek mode is used for changing the position of the seek line on mouse click.
	* Block mode is used for entering blocks to the widget via mouse click.
	* Delete mode is used for deleting blocks from the widget via mouse click.
	*/
	inputModes = ["SEEK","BLOCK","DELETE"];
	/**
	* The mode that the widget is currently in.
	*/
	inputMode = "BLOCK";

	/**
	* Width of a cell in local, i.e., not screen coords.
	*/
	localWidth = 0;
	/**
	* Height of a cell in local, i.e., not screen coords.
	*/
	localHeight = 0;

	// TODO: We should factor common code from this into reset and just call reset here instead.
	/**
	* Construct a tracklane canvas widget instance and draw it to the screen.
	* @param {string} query - String containing html id of the canvas we are constructing for.
	* @param {number} horizontalCells - The number of horizontal cells to draw.
	* @param {number} verticalCells - The number of vertical cells to draw.
	*/
	constructor(query,horizontalCells,verticalCells)
	{
		// Set Up the canvas
		this.canvas = document.getElementById(query);
		this.ctx = this.canvas.getContext("2d");

		// The height offset for the buttons and tabs of our gui
		let tabsHeight = 2*document.getElementById('tab-container').offsetHeight;
		tabsHeight += document.getElementById("track-controls").offsetHeight;

		// set up canvas and local coord dimensions
		this.canvas.width = window.innerWidth;
		this.canvas.height = window.innerHeight - tabsHeight;
		this.localWidth = 500;
		this.localHeight = 500;
		
		// Set up cell sizes
		this.verticalCells = verticalCells;
		this.horizontalCells = horizontalCells;
	
		// Compute widths and heights of cells
		this.cellWidth = this.localWidth/this.verticalCells; // the number of vertical cell divisions controls cell widths
		this.cellHeight = this.localHeight/this.horizontalCells; // the number of horizontal cell divisions controls cell heights

		// set up font height
		this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
		while (this.ctx.measureText('@').width < this.cellHeight) // The width of @ approximates height
		{
			this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
			this.rectangleFontSize++;
		}
		this.rectangleFontSize = 0.75*this.rectangleFontSize; // We scale by a padding factor of 75% for vertical fitting

		var that = this;
		this.canvas.addEventListener('mousedown', function(ev) { that.leftClickDown(); }); 
		this.canvas.addEventListener('mouseup', function(ev) { that.leftClickUp(); }); 
		this.canvas.addEventListener('keydown', function(ev) { that.buttonClick(ev); });
		this.canvas.addEventListener('mousemove', function(ev) { that.updateMouseCoordinates(); }); 

		// this displays the track area nicely on my screen
		this.ctx.scale(this.scaleAmtX,1);
		this.ctx.scale(this.scaleAmtX,1);
		this.ctx.scale(this.scaleAmtX,1);
		this.ctx.scale(this.scaleAmtX,1);
		this.ctx.scale(1,1/this.scaleAmtY);
		this.ctx.translate(this.translateAmt,0);
		this.ctx.translate(0,this.translateAmt);
		// do the first draw
		this.draw();
	}

	/**
	* Resets the state of the canvas.
	* Most often this function is used to change the number of horizontal and vertical cells.
	* @param {number} horizontalCells - The number of horizontal cells to draw.
	* @param {number} verticalCells - The number of vertical cells to draw.
	*/
	reset(horizontalCells,verticalCells)
	{
		this.coord = {x:0, y:0}; // the coords of the mouse
		this.leftClickStart = {x:0, y:0}; // the coords of the mouse at the start of a click
		this.leftClickEnd = {x:0, y:0}; // the coords of the mouse at the release of a click
		this.mousePressed = false; //record whether the mouse has been pressed
		this.trackList = new Array(); // tracks are arrays of rectangles specified by their top left and bottom right coords
		this.workingRectangle = null; // A rectangle not yet saved in the tracklist
		this.existingCollision = false; // flag tracking whether the user has clicked an existing rectangle
		this.moveIndex = -1; // the index that the collision occurred at 
		this.inputMode = "BLOCK";
		this.blockSize = 1; // length of the rectangle to draw
		this.rectangleFontSize = 1;

		// Set up cell sizes
		this.verticalCells = verticalCells;
		this.horizontalCells = horizontalCells;
	
		// Compute widths and heights of cells
		this.cellWidth = this.localWidth/this.verticalCells; // the number of vertical cell divisions controls cell widths
		this.cellHeight = this.localHeight/this.horizontalCells; // the number of horizontal cell divisions controls cell heights

		// set up font height
		this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
		while (this.ctx.measureText('@').width < this.cellHeight) // The width of @ approximates height
		{
			this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
			this.rectangleFontSize++;
		}
		this.rectangleFontSize = 0.75*this.rectangleFontSize; // We scale by a padding factor of 75% for vertical fitting

		// do the first draw
		this.draw();
	}

	/**
	* Handles button clicks from the user.
	* @param {event} ev - The event containing the button click we are handling.
	*/
	buttonClick(ev)
	{
		let controlText = "";
			controlText += "h: display keybinds\n";
			controlText += "wasd: scroll viewport\n";
			controlText += "qe: scale viewport in X\n";
			controlText += "zc: scale viewport in X\n";
			controlText += "rf: change amount to translate by\n";
			controlText += "tg: change X scaling amount\n";
			controlText += "yh: change Y scaling amount\n";
			controlText += "=-: increment/decrement block sizes\n";
			//controlText += "ctrl: toggle block/delete modes\n";
			controlText += "1/2/3: switch between block/seek/delete mode\n";
			controlText += "4: reset the seek line position\n";

		if (ev.key == "1") 
		{
			this.inputMode = "BLOCK";
			this.workingRectangle = null;
			this.draw();
		}
		else if (ev.key == "2") 
		{
			this.inputMode = "SEEK";
			this.workingRectangle = null;
			this.draw();
		}
		else if (ev.key == "3") 
		{
			this.inputMode = "DELETE";
			this.workingRectangle = null;
			this.draw();
		}
		else if (ev.key == "4")
		{
			this.seekPos = {x:0, y:0};
			this.draw();
		}
		else if (ev.key == "h") alert(controlText);
		else if (ev.key == "q") this.ctx.scale(this.scaleAmtX,1);
		else if (ev.key == "e") this.ctx.scale(1/this.scaleAmtX,1);
		else if (ev.key == "z") this.ctx.scale(1,this.scaleAmtY);
		else if (ev.key == "c") this.ctx.scale(1,1/this.scaleAmtY);
		else if (ev.key == "a") this.ctx.translate(this.translateAmt,0);
		else if (ev.key == "d") this.ctx.translate(-this.translateAmt,0);
		else if (ev.key == "s") this.ctx.translate(0,-this.translateAmt);
		else if (ev.key == "w") this.ctx.translate(0,this.translateAmt);
		else if (ev.key == "r") this.translateAmt += 10;
		else if (ev.key == "f") this.translateAmt -= 10;
		else if (ev.key == "t") this.scaleAmtX *= (1+1/(2**4));
		else if (ev.key == "g") this.scaleAmtX /= (1+1/(2**4));
		else if (ev.key == "y") this.scaleAmtY *= (1+1/(2**4));
		else if (ev.key == "h") this.scaleAmtY /= (1+1/(2**4));
		this.draw();
	}

	/**
	* Snap input coordinates to grid and return the resulting coord
	* @param {number} c - the coordinate to snap to the grid.
	* @returns The coordinate resulting from snapping c to the grid.
	*/
	snapToGrid(c)
	{
		var out = {
			x: this.cellWidth * Math.floor(c.x/this.cellWidth),
   			y: this.cellHeight * Math.floor(c.y/this.cellHeight)
		};
		return out;
	}

	/**
	* Handle when mouse left click is pressed down.
	*/
	leftClickDown()
	{
		this.mousePressed = true;

		// run the delete handler if in delete mode
		if (this.inputMode == "DELETE") 
		{
			this.deleteModeLeftClickDown();
			this.draw();
			return;
		}
		else if (this.inputMode == "SEEK") // if in seek mode we only need to change the seek line position
		{
			this.seekPos.x = this.screenToWorldCoords(this.coord).x;
			this.draw();
			return;
		}

		this.leftClickStart = this.snapToGrid(this.screenToWorldCoords(this.coord));

		// check if a collision has occurred
		let c = {x:this.leftClickStart.x+this.cellWidth/2, 
					y:this.leftClickStart.y+this.cellHeight/2};
		for (let i = 0; i < this.trackList.length; i++)
		{
			// The test point needs to be partially inside the cell to avoid edge case problems
			let c = {x:this.leftClickStart.x+this.cellWidth/2, 
						y:this.leftClickStart.y+this.cellHeight/2};
			if (this.rectangleCollision(c,this.trackList[i]))
			{
				this.existingCollision = true;
				this.moveIndex = i;
				return;
			}
		}

		// calculate the dimensions of the block to be inserted
		this.leftClickEnd.x = this.leftClickStart.x + (this.cellWidth * this.blockSize);
		this.leftClickEnd.y = this.leftClickStart.y + this.cellHeight;

		// the working rectangle
		this.workingRectangle = new Array(this.leftClickStart,this.leftClickEnd);
		this.draw();
	}
	/**
	* Handle left click presses while in delete mode.
	*/
	deleteModeLeftClickDown()
	{
		this.mousePressed = false;
		let c = this.screenToWorldCoords(this.coord);
		for (let i = 0; i < this.trackList.length; i++)
    		if (this.rectangleCollision(c,this.trackList[i])) // if cursor lies inside a rectangle
    		{
        		this.trackList.splice(i,1); // remove the rectangle
        		break;
    		}
		this.draw();
	}

	/**
	* Handle release of mouse left click.
	*/
	leftClickUp()
	{
		this.mousePressed = false;

		if (this.inputMode == "SEEK" || this.inputMode == "DELETE") return;

		if (this.existingCollision)
		{
			let i = this.moveIndex;

			// Save the entry of the track list that we are attempting to move
			let tempLeft  = {x:this.trackList[i][0].x, y:this.trackList[i][0].y};
			let tempRight  = {x:this.trackList[i][1].x, y:this.trackList[i][1].y};
			let tempName = this.trackList[i][2];
			
			// remove it from the current track list
			this.trackList.splice(i,1);

			// check if its new position collides with any of the other trackList entries
			let collision = this.rectCollisionCheck(tempLeft,tempRight);

			// if no collision occurred we can readd it, otherwise it will be deleted
			if (!collision) this.trackList.push([tempLeft,tempRight,tempName]);

			// We are done moving the track
			this.existingCollision = false;
			this.moveIndex = -1;
			this.draw();
			return;
		}

		// Check if the new rectangle is colliding with any of the existing ones
		let collision = this.rectCollisionCheck(this.leftClickStart,this.leftClickEnd);

		// The coords of the new rectangle to insert in the list
		let newLeft = {x:this.leftClickStart.x, y:this.leftClickStart.y};
		let newRight = {x:this.leftClickEnd.x, y:this.leftClickEnd.y};
		// If no collisions occur we can insert the finished rectangle
		if (!collision) this.trackList.push([newLeft,newRight,this.blockName]);
		this.workingRectangle = null;
		this.draw();
	}
	
	/**
	* Checks if point pt lies inside rectangle rect.
	* @param {object} pt - Point to test for inclusion.
	* @param {object} track - Rectangle/track (array containing topleft/bottom right coords) to test inclusion of pt against.
	* @returns true or false depending on if pt lies in rect.
	*/
	rectangleCollision(pt,track)
	{
		return (track[0].x <= pt.x && pt.x <= track[1].x && track[0].y <= pt.y && pt.y <= track[1].y);
	}

	/**
	* Update the current coordinates of the mouse.
	*/
	updateMouseCoordinates()
	{
		this.coord.x = event.clientX - this.canvas.offsetLeft; 
		this.coord.y = event.clientY - this.canvas.offsetTop; 


		// If the click is to move a block
		if (this.existingCollision)
		{
			let i = this.moveIndex;
			let length = this.trackList[i][1].x - this.trackList[i][0].x
			this.leftClickStart = this.snapToGrid(this.screenToWorldCoords(this.coord));
			this.trackList[i][0].x = this.leftClickStart.x;
			this.trackList[i][0].y = this.leftClickStart.y;
			this.trackList[i][1].x = this.leftClickStart.x + length;
			this.trackList[i][1].y = this.leftClickStart.y+this.cellHeight;
			this.draw();
			return;
		}

		if (this.mousePressed)
		{	
			// set up left click coords
			//this.rectangleHelper();
			
			// Calculate the new mouse coordinates
			this.leftClickStart = this.snapToGrid(this.screenToWorldCoords(this.coord));
			this.leftClickEnd.x = this.leftClickStart.x + (this.cellWidth * this.blockSize);
			this.leftClickEnd.y = this.leftClickStart.y + this.cellHeight;

			// If the mouse is pressed and held there is stuff to draw
			if (this.mousePressed && this.workingRectangle != null) 
			{
				this.workingRectangle[0] = this.leftClickStart;
				this.workingRectangle[1] = this.leftClickEnd;
				this.draw();
			}
		}
	}	

	/**
	* Draw the current state of the widget to the screen.
	*/
	draw()
	{
		// First we need to clear the old background 

		// Store the current transformation matrix
		this.ctx.save();

		// Use the identity matrix while clearing the canvas
		this.ctx.setTransform(1, 0, 0, 1, 0, 0);
		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

		// Restore the transform
		this.ctx.restore();

		// Now we can actually start drawing

		//draw vertical divisions
		for (var i = 0; i < this.verticalCells; i++)
		{
			this.ctx.strokeStyle = 'black';
			this.ctx.lineWidth = this.lineWidth;
			this.ctx.beginPath();
			this.ctx.moveTo(i*(this.localWidth/this.verticalCells),0);
			this.ctx.lineTo(i*(this.localWidth/this.verticalCells),this.localHeight);
			this.ctx.stroke();
		}

		//draw horizontal divisions
		for (var i = 0; i < this.horizontalCells; i++)
		{
			this.ctx.strokeStyle = 'black';
			this.ctx.lineWidth = this.lineWidth;
			this.ctx.beginPath();
			this.ctx.moveTo(0,i*(this.localHeight/this.horizontalCells));
			this.ctx.lineTo(this.localWidth,i*(this.localHeight/this.horizontalCells));
			this.ctx.stroke();
		}

		// draw rectangles
		for (let i = 0; i < this.trackList.length; i++)
		{
			let c1 = this.trackList[i][0];
			let c2 = this.trackList[i][1];
			let name = this.trackList[i][2];
			this.drawRectangle(c1,c2,name);
		}
		if (this.workingRectangle != null)
			this.drawRectangle(this.workingRectangle[0],this.workingRectangle[1],this.blockName);

		// draw seek line
		this.ctx.strokeStyle = 'red';
		this.ctx.lineWidth = this.seekLineWidth;
		this.ctx.beginPath();
		this.ctx.moveTo(this.seekPos.x,0);
		this.ctx.lineTo(this.seekPos.x,this.localHeight);
		this.ctx.stroke();

		// Draw the outlines for the canvas too
		this.drawRectangleOutline({x:0,y:0},{x:this.localWidth,y:this.localHeight});

		// Now we want to draw the outlines for the helper text on top of the canvas
		// Store the current transformation matrix
		this.ctx.save();

		// Use the identity matrix while clearing the canvas
		this.ctx.setTransform(1, 0, 0, 1, 0, 0);

		// Draw outline and helper text to fixed positions in viewport
		this.helperText();
		this.drawRectangleOutline({x:0,y:0},{x:this.canvas.width,y:this.canvas.height});

		// Restore the transform
		this.ctx.restore();

	}
	/**
	* Draw a rectangle with the given points and color.
	* @param {object} c1 - Object denoting top left coord of rectangle.
	* @param {object} c2 - Object denoting bottom right coord of rectangle.
	* @param {string} name - String containing the name of the rectangle.
	*/
	drawRectangle(topLeft,bottomRight,name)
	{
		// Now we can draw the rectangle 
		this.ctx.fillStyle = "rgb(0 0 255)";
		this.ctx.beginPath();
		this.ctx.moveTo(topLeft.x,topLeft.y);
		this.ctx.lineTo(topLeft.x,bottomRight.y);
		this.ctx.lineTo(bottomRight.x,bottomRight.y);
		this.ctx.lineTo(bottomRight.x,topLeft.y);
		this.ctx.fill();
	
		// Draw rectangle outlines
		this.ctx.beginPath();
		this.ctx.moveTo(topLeft.x,topLeft.y);
		this.ctx.lineTo(topLeft.x,bottomRight.y);
		this.ctx.lineTo(bottomRight.x,bottomRight.y);
		this.ctx.lineTo(bottomRight.x,topLeft.y);
		this.ctx.lineTo(topLeft.x,topLeft.y);
		this.ctx.lineWidth = this.lineWidth;
		this.ctx.strokeStyle = 'black';
		this.ctx.stroke();

		// draw the text
		this.ctx.lineWidth = this.lineWidth;
		this.ctx.strokeStyle = 'black';
		this.ctx.stroke();

		// get the correct height
		this.ctx.textBaseline = "bottom";
		this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
		this.ctx.fillStyle = 'black';
		this.ctx.fillText(name,topLeft.x,topLeft.y+this.cellHeight,Math.abs(topLeft.x-bottomRight.x)*0.90);
		this.ctx.textBaseline = "alphabetic";
	}
	/**
	* Draw a rectangle outline with the given points.
	* @param {object} c1 - Object denoting top left coord of rectangle.
	* @param {object} c2 - Object denoting bottom right coord of rectangle.
	*/
	drawRectangleOutline(c1,c2)
	{
		this.ctx.beginPath();
		this.ctx.moveTo(c1.x,c1.y);
		this.ctx.lineTo(c1.x,c2.y);
		this.ctx.lineTo(c2.x,c2.y);
		this.ctx.lineTo(c2.x,c1.y);
		this.ctx.lineTo(c1.x,c1.y);
		this.ctx.lineWidth = this.lineWidth;
		this.ctx.strokeStyle = 'black';
		this.ctx.stroke();
	}

	// TODO: This should probably be renamed to match the other widgets.
	/**
	* Helper that sets up leftClickEnd and leftClickStarts coordinates.
	*/
	rectangleHelper()
	{
		// set up left click coords
		this.leftClickEnd = this.screenToWorldCoords(this.coord);

		// snap to the grid
		// Line the two x coords up to snap to the appropriate rectangle edges
		if (this.leftClickStart.x <= this.leftClickEnd.x) 
			this.leftClickEnd.x += this.cellWidth;
		else 
			this.leftClickEnd.x = this.leftClickStart.x+this.cellWidth;
		
		// Line the two y coords up to snap to the appropriate rectangle edges
		if (this.leftClickStart.y <= this.leftClickEnd.y)
			this.leftClickEnd.y += this.cellHeight;
		else
			this.leftClickStart.y += this.cellHeight;
		
		// snap to the grid
		this.leftClickEnd = this.snapToGrid(this.leftClickEnd);
	}

	/**
	* Used for incrementing blocksize variable.
	*/
	incrementBlockSize()
	{
		this.blockSize++;
		this.draw();
	}
	/**
	* Used for incrementing blocksize variable.
	*/
	decrementBlockSize()
	{
		if (this.blockSize > 1) this.blockSize--;
		this.draw();
	}

	/**
	* This function is used to check if the rectangle specified by [left,right]
	* collides with (i.e. intersects) any other rectangles in our list of rectangles.
	* This function returns true if so and false otherwise.
	* @param {object} left - The left endpoint of the rectangle.
	* @param {object} right - The right endpoint of the rectangle.
	* @returns True or false depending on if the input rectangle intersects any of the rectangles in our track list so far.
	*/
	rectCollisionCheck(left,right)
	{
		// the return value
		let collision = false;

		// check left endpoint collisions
		for (let i = 0; i < this.trackList.length; i++)
		{
			// The test point needs to be partially inside the cell to avoid edge case problems
			let c = {x:left.x+this.cellWidth/2, y:left.y+this.cellHeight/2};
			if (this.rectangleCollision(c,this.trackList[i])) collision = true;
		}

		// Check the following types of collisions at the right end point:
		// 1: Right end point of working rectangle in previous rectangle
		// 2: Left end point of previous rectangle in working rectangle
		for (let i = 0; i < this.trackList.length; i++)
		{
			// The test point needs to be partially inside the cell to avoid edge case problems
			let cTest = {x:right.x-this.cellwidth/2, y:right.y-this.cellHeight/2};
			// same idea for the track list point
			let tTest = {x:this.trackList[i][0].x+this.cellWidth/2, 
							y:(this.trackList[i][0].y+this.trackList[i][1].y)/2};
			if (this.rectangleCollision(cTest,this.trackList[i]))
				collision = true;
			else if (this.rectangleCollision(tTest,[left,right]))
				collision = true;
		}
		return collision;
	}


	/**
	* Converts the coordinates of the input point in screen coordinates to local/world coordinates.
	* @param {object} p - Point to convert.
	* @returns A new point with transformed x and y coords.
	*/
	screenToWorldCoords(p)
	{
		// get and invert the canvas xform coords, then apply them to the input point
		return this.ctx.getTransform().invertSelf().transformPoint(p);
	}


	/**
	* Prints helper text to the top right corner of the widget.
	*/
	helperText()
	{
		// Draw text showing the mode
		let text = ""
		if (this.inputMode == "SEEK") text = "Seek mode. ";
		else if (this.inputMode == "BLOCK") text = "Block mode. ";
		else if (this.inputMode == "DELETE") text = "Delete mode. ";
		else text = "Unknown mode. ";
		text += "Press h for keybinds.";
	
		this.ctx.font = "bold 25px Arial";
		this.ctx.fillStyle = 'black';
		let textHeight = this.ctx.measureText('M').width; // The width of capital M approximates height
		let textWidth = this.ctx.measureText(text).width;
		this.ctx.fillText(text,this.canvas.width-textWidth,textHeight);
		text = "block size: " + this.blockSize + ", block name: "+this.blockName;
		textWidth = this.ctx.measureText(text).width;
		this.ctx.fillText(text,this.canvas.width-textWidth,2*textHeight);
		text = "x zoom amount: " + this.scaleAmtX.toFixed(2);
		text += ", y zoom amount: " + this.scaleAmtY.toFixed(2);
		textWidth = this.ctx.measureText(text).width;
		this.ctx.fillText(text,this.canvas.width-textWidth,3*textHeight);
		text = "translate amount: " +this.translateAmt;
		textWidth = this.ctx.measureText(text).width;
		this.ctx.fillText(text,this.canvas.width-textWidth,4*textHeight);
	}
	/**
	* Used for setting the block size variable.
	* @param {number} sz - The new block size.
	*/
	setBlockSize(sz)
	{
		this.blockSize = sz;
		this.draw();
	}
	/**
	* Used for setting the block name variable.
	* @param {number} sz - The new block name.
	*/
	setBlockName(name)
	{
		this.blockName = name;
		this.draw();
	}

	// TODO: I think the paramList can be deleted here.
	/**
	* Creates an array containing the name of each block and its start offset in seconds.
	* @param {number} bpm - The beats per minute value to use for time conversion.
	* @param {number} bpb - The beats per block value to use for time conversion.
	* @param {number} paramList - This parameter is deprecated and will be removed in later versions
	* @returns An array of tuples containing each track name and its start offset time in seconds.
	*/
	getOffsetsAndNames(bpm,bpb,paramList)
	{
		// The array we will output to
		let outArr = new Array();
		// Convert start time to offset cell time
		for (let i = 0; i < this.trackList.length; i++)
		{
			let offsetCell = this.coordToCell(this.trackList[i][0].x);
			let offsetTime = this.cellsToSeconds(offsetCell,bpm,bpb);
			let name = this.trackList[i][2];
			outArr.push([name,offsetTime]);
		}

		return outArr;
	}
	/**
	* Round an x coord to its cell value.
	* @param {number} n - x coord to round.
	* @returns the corresponding cell value of n.
	*/
	coordToCell(n)
	{
		return Math.round(n/this.cellWidth);
	}
	/**
	* Convert a raw cell number to a value in seconds.
	* @param {number} c - The cell number to convert.
	* @param {number} bpm - Beats per minute, required to do unit conversion of times.
	* @param {number} bpb - Beats per block, required to do unit conversion of times.
	* @returns Converted value described above.
	*/
	cellsToSeconds(c,bpm,bpb)
	{
		// the start time in seconds
		let cellsPerSecond = bpm * (1/60);
		return bpb * c / cellsPerSecond;
	}
	// Get the horizontal position of the seek bar in seconds
	/**
	* Get the horizontal position of the seek bar in seconds
	* @param {number} bpm - Beats per minute, required to do unit conversion of times.
	* @param {number} bpb - Beats per block, required to do unit conversion of times.
	* @returns The position of the seekbar in seconds.
	*/
	seekToSeconds(bpm,bpb)
	{
		//return this.cellsToSeconds(this.seekPos.x,bpm,bpb);
		let val = this.coordToCell(this.seekPos.x);
		return this.cellsToSeconds(val,bpm,bpb);
	}
	/**
	* Set up the state of the widget based on the input argument.
	* @param {object} state - The state used to configure the widget.
	*/
	reconfigure(state)
	{
		//coord = {x:0, y:0}; // the coords of the mouse
		//leftClickStart = {x:0, y:0}; // the coords of the mouse at the start of a click
		//leftClickEnd = {x:0, y:0}; // the coords of the mouse at the release of a click
		//mousePressed = false; //record whether the mouse has been pressed
		this.trackList = state.trackList;
		this.workingRectangle = state.trackList;
		//existingCollision = false; // flag tracking whether the user has clicked an existing rectangle
		//moveIndex = -1; // the index that the collision occurred at 
		//this.inputMode = state.inputMode;
		this.blockSize = state.blockSize;
		this.blockName = state.blockName;
	
		// values for changing the scale and translate amount
		this.translateAmt = state.translateAmt;
		this.scaleAmtX = state.scaleAmtX;
		this.scaleAmtY = state.scaleAmtY;
		
		// Set up cell sizes
		this.localHeight = state.localHeight;
		this.localWidth = state.localWidth;
		this.verticalCells = state.verticalCells;
		this.horizontalCells = state.horizontalCells;
	
		// Compute widths and heights of cells
		this.cellWidth = this.localWidth/this.verticalCells; // the number of vertical cell divisions controls cell widths
		this.cellHeight = this.localHeight/this.horizontalCells; // the number of horizontal cell divisions controls cell heights

		// set up font height
		this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
		while (this.ctx.measureText('@').width < this.cellHeight) // The width of @ approximates height
		{
			this.ctx.font = "bold "+this.rectangleFontSize+"px Arial";
			this.rectangleFontSize++;
		}
		this.rectangleFontSize = 0.75*this.rectangleFontSize; // We scale by a padding factor of 75% for vertical fitting
		this.draw();
	}
}