/*
	Cinnabar Graphics Library - A library of direct-drawing functions designed for use with JavaScript imageData objects
	2025 by Mercury Thirteen, Creative Commons Attribution 4.0 International License

	Revision history:
	- Added pixmap functions to lessen dependence upon .getImageData() and .putImageData().
	- Drawing functions now clip to the dimensions of the pixmap into which they draw.

	2025-01-17
	- Added bounds checking to CGLSet().
	- Added CGLTextChar() and CGLTextString() for font printing support.
	- Removed CGLEllipseRect() as its functionality is doable with other existing functions

	2025-01-14
	- Fixed an issue where the incorrect circle drawing routine was left in place after testing.

	2025-01-09
	- Initial release
*/





function CGLRectUnion(rect1, rect2)
{
	var outputRect = {};

	outputRect.top		= rect1.top;
	outputRect.left		= rect1.left;
	outputRect.bottom	= rect1.bottom;
	outputRect.right	= rect1.right;

	if (outputRect.top		> rect2.top)		outputRect.top		= rect2.top;
	if (outputRect.left		> rect2.left)		outputRect.left		= rect2.left;
	if (outputRect.bottom	< rect2.bottom)		outputRect.bottom	= rect2.bottom;
	if (outputRect.right	< rect2.right)		outputRect.right	= rect2.right;

	return outputRect;
}





function CGLPixmapMakeNative(thePixmap)
{
	// Backfill some data that one of our pixmaps would have already had

	if (thePixmap.bytesPerPixel	=== undefined) thePixmap.bytesPerPixel	= 4;
	if (thePixmap.bytesPerLine	=== undefined) thePixmap.bytesPerLine	= thePixmap.width * thePixmap.bytesPerPixel;

	if (thePixmap.maxX === undefined) thePixmap.maxX = thePixmap.width	- 1;
	if (thePixmap.maxY === undefined) thePixmap.maxY = thePixmap.height	- 1;

	return thePixmap;
}





function CGLPixmapNew(width, height, bitsPerPixel)
{
	var bytesPerPixel = Math.floor(bitsPerPixel / 8);
	if ((bitsPerPixel / 8) != bytesPerPixel) bytesPerPixel++;

	thisPixmap					= {};
	thisPixmap.width			= width;
	thisPixmap.height			= height;
	thisPixmap.maxX				= width		- 1;
	thisPixmap.maxY				= height	- 1;
	thisPixmap.bitsPerPixel		= bitsPerPixel;
	thisPixmap.bytesPerPixel	= bytesPerPixel;
	thisPixmap.bytesPerLine		= bytesPerPixel * width;
	thisPixmap.data				= new Uint8ClampedArray(height * thisPixmap.bytesPerLine);

	return thisPixmap;
}





function CGLPixmapCopy(sourcePixmap, destPixmap, sourceRect, destPoint)
{
	var sourceRectX			= sourceRect.x;
	var sourceRectY			= sourceRect.y;
	var sourceRectWidth		= sourceRect.width;
	var sourceRectHeight	= sourceRect.height;
	var destPointX			= destPoint.x;
	var destPointY			= destPoint.y;

	sourcePixmap	= CGLPixmapMakeNative(sourcePixmap);
	destPixmap		= CGLPixmapMakeNative(destPixmap);

	// No sense to hang around if the point specified is beyond the bounds of the pixmap.
	if (destPointX > destPixmap.maxX) return;
	if (destPointY > destPixmap.maxY) return;

	// // Clip sourceRect if it will extend past the right or bottom edges of destPixmap.
	// if ((destPointX + sourceRectWidth) > destPixmap.maxX) sourceRectRight	= sourceRectLeft	+ destPixmap.maxX - destPointX;
	// if ((destPointY + sourceRectHeight) > destPixmap.maxY) sourceRectBottom	= sourceRectTop		+ destPixmap.maxY - destPointY;

	// if (destPointX < 0)
	// {
	// 	if ((destPointX + (sourceRectRight - sourceRectLeft)) >= 0)
	// 	{
	// 		// If we get here, some portion of sourceRect will be visible even though destPointX was negative, so we need to clip sourceRect accordingly.
	// 		sourceRectLeft = sourceRectLeft - destPointX;
	// 		destPointX = 0;
	// 	} else {
	// 		return;
	// 	}
	// }

	// if (destPointY < 0)
	// {
	// 	if ((destPointY + (sourceRectBottom - sourceRectTop)) >= 0)
	// 	{
	// 		// If we get here, some portion of sourceRect will be visible even though destPointX was negative, so we need to clip sourceRect accordingly.
	// 		sourceRectTop = sourceRectTop - destPointY;
	// 		destPointY = 0;
	// 	} else {
	// 		return;
	// 	}
	// }


	// // Clip sourceRect to not extend past the boundaries of sourcePixmap.
	// if (sourceRectTop	< 0) sourceRectTop	= 0;
	// if (sourceRectLeft	< 0) sourceRectLeft	= 0;

	// if (sourceRectBottom	> sourcePixmap.maxY)	sourceRectBottom	= sourcePixmap.maxY;
	// if (sourceRectRight		> sourcePixmap.maxX)	sourceRectRight		= sourcePixmap.maxX;

	var byteCount		= sourceRectWidth * sourcePixmap.bytesPerPixel;
	var sourceBytePtr	= ((sourceRectY	* sourcePixmap.width)	+ sourceRectX)	* sourcePixmap.bytesPerPixel;
	var destBytePtr		= ((destPointY	* destPixmap.width)		+ destPointX)	* destPixmap.bytesPerPixel;

	for (var i = 0; i < sourceRectHeight; i++)
	{
		// copy a single line of pixels
		var thisSourceBytePtr	= sourceBytePtr;
		var thisDestBytePtr		= destBytePtr;

		for (var j = 0; j < byteCount; j++)
		{
			destPixmap.data[thisDestBytePtr++] = sourcePixmap.data[thisSourceBytePtr++];
		}

		sourceBytePtr	= sourceBytePtr	+ sourcePixmap.bytesPerLine;
		destBytePtr		= destBytePtr	+ destPixmap.bytesPerLine;
	}
}





function CGLPixmapCopyWithAlpha(sourcePixmap, destPixmap, sourceRect, destPoint)
{
	var sourceRectX			= sourceRect.x;
	var sourceRectY			= sourceRect.y;
	var sourceRectWidth		= sourceRect.width;
	var sourceRectHeight	= sourceRect.height;
	var destPointX			= destPoint.x;
	var destPointY			= destPoint.y;

	sourcePixmap	= CGLPixmapMakeNative(sourcePixmap);
	destPixmap		= CGLPixmapMakeNative(destPixmap);

	// No sense to hang around if the point specified is beyond the bounds of the pixmap.
	if (destPointX > destPixmap.maxX) return;
	if (destPointY > destPixmap.maxY) return;

	// // Clip sourceRect if it will extend past the right or bottom edges of destPixmap.
	// if ((destPointX + sourceRectRight	- sourceRectLeft)	> destPixmap.maxX) sourceRectRight	= sourceRectLeft	+ destPixmap.maxX - destPointX;
	// if ((destPointY + sourceRectBottom	- sourceRectTop)	> destPixmap.maxY) sourceRectBottom	= sourceRectTop		+ destPixmap.maxY - destPointY;

	// // 
	// if (destPointX < 0)
	// {
	// 	if ((destPointX + (sourceRectRight - sourceRectLeft)) >= 0)
	// 	{
	// 		// If we get here, some portion of sourceRect will be visible even though destPointX was negative, so we need to clip sourceRect accordingly.
	// 		sourceRectLeft	= sourceRectLeft - destPointX;
	// 		destPointX		= 0;
	// 	} else {
	// 		return;
	// 	}
	// }

	// if (destPointY < 0)
	// {
	// 	if ((destPointY + (sourceRectBottom - sourceRectTop)) >= 0)
	// 	{
	// 		// If we get here, some portion of sourceRect will be visible even though destPointX was negative, so we need to clip sourceRect accordingly.
	// 		sourceRectTop	= sourceRectTop - destPointY;
	// 		destPointY		= 0;
	// 	} else {
	// 		return;
	// 	}
	// }

	// // Clip sourceRect to not extend past the boundaries of sourcePixmap.
	// if (sourceRectTop	< 0) sourceRectTop	= 0;
	// if (sourceRectLeft	< 0) sourceRectLeft	= 0;

	// if (sourceRectBottom	> sourcePixmap.maxY)	sourceRectBottom	= sourcePixmap.maxY;
	// if (sourceRectRight		> sourcePixmap.maxX)	sourceRectRight		= sourcePixmap.maxX;

	var sourceBytePtr	= ((sourceRectY	* sourcePixmap.width)	+ sourceRectX)	* sourcePixmap.bytesPerPixel;
	var destBytePtr		= ((destPointY	* destPixmap.width)		+ destPointX)	* destPixmap.bytesPerPixel;


	for (var i = 0; i < sourceRectHeight; i++)
	{
		// copy a single line of pixels
		var thisSourceBytePtr	= sourceBytePtr;
		var thisDestBytePtr		= destBytePtr;

		for (var j = 0; j < sourceRectWidth; j++)
		{
			var thisAlpha = sourcePixmap.data[thisSourceBytePtr + 3];
			if (thisAlpha == 0xFF)
			{
				destPixmap.data[thisDestBytePtr++] = sourcePixmap.data[thisSourceBytePtr++];
				destPixmap.data[thisDestBytePtr++] = sourcePixmap.data[thisSourceBytePtr++];
				destPixmap.data[thisDestBytePtr++] = sourcePixmap.data[thisSourceBytePtr++];
				destPixmap.data[thisDestBytePtr++] = sourcePixmap.data[thisSourceBytePtr++];
			} else {
				if (thisAlpha == 0x00)
				{
					thisSourceBytePtr	= thisSourceBytePtr + 4;
					thisDestBytePtr		= thisDestBytePtr + 4;
				} else {
					// Blend here
				}
			}
		}

		sourceBytePtr	= sourceBytePtr	+ sourcePixmap.bytesPerLine;
		destBytePtr		= destBytePtr	+ destPixmap.bytesPerLine;
	}
}





function CGLPixmapResize(sourcePixmap, newWidth, newHeight)
{

}
















































function CGL_CircleHelper(imgDataObject, x0, y0, r, cornerCode, colorObject)
{
	var f = 1 - r;
	var ddF_x = 1;
	var ddF_y = -2 * r;
	var x = 0;
	var y = r;

	while (x < y)
	{
		if (f >= 0)
		{
			y--;
			ddF_y += 2;
			f += ddF_y;
		}

		x++;
		ddF_x += 2;
		f += ddF_x;

		if (cornerCode & 4)
		{
			CGLSet(imgDataObject, x0 + x, y0 + y, colorObject);
			CGLSet(imgDataObject, x0 + y, y0 + x, colorObject);
		}

		if (cornerCode & 2)
		{
			CGLSet(imgDataObject, x0 + x, y0 - y, colorObject);
			CGLSet(imgDataObject, x0 + y, y0 - x, colorObject);
		}

		if (cornerCode & 8)
		{
			CGLSet(imgDataObject, x0 - y, y0 + x, colorObject);
			CGLSet(imgDataObject, x0 - x, y0 + y, colorObject);
		}

		if (cornerCode & 1)
		{
			CGLSet(imgDataObject, x0 - y, y0 - x, colorObject);
			CGLSet(imgDataObject, x0 - x, y0 - y, colorObject);
		}
	}
}





function CGL_CircleFilledHelper (imgDataObject, x0, y0, r, corners, delta, colorObject)
{
	var f = 1 - r;
	var ddF_x = 1;
	var ddF_y = -2 * r;
	var x = 0;
	var y = r;
	var px = x;
	var py = y;

	delta++;

	while (x < y)
	{
		if (f >= 0)
		{
			y--;
			ddF_y += 2;
			f += ddF_y;
		}

		x++;

		ddF_x += 2;
		f += ddF_x;

		if (x < (y + 1))
		{
			if (corners & 1) CGLLineVertical(imgDataObject, x0 + x, y0 - y, 2 * y + delta, colorObject);
			if (corners & 2) CGLLineVertical(imgDataObject, x0 - x, y0 - y, 2 * y + delta, colorObject);
		}

		if (y != py)
		{
			if (corners & 1) CGLLineVertical(imgDataObject, x0 + py, y0 - px, 2 * px + delta, colorObject);
			if (corners & 2) CGLLineVertical(imgDataObject, x0 - py, y0 - px, 2 * px + delta, colorObject);
			py = y;
		}

		px = x;
	}
}





function CGL_Interpolate(i0, d0, i1, d1)
{
	if (i0 == i1)
	{
		return [d0];
	}

	values = [];
	a = (d1 - d0) / (i1 - i0);
	d = d0;

	for (var i = i0; i <= i1; i++)
	{
		values.push(Math.round(d));
		d = d + a;
	}

	return values;
}





function CGLCircle(imgDataObject, xPos, yPos, radius, colorObject)
{
	// https://github.com/zingl/Bresenham/blob/master/bresenham.js
	var x = -radius
	var y = 0
	var err = 2 - 2 * radius;
	do {
		CGLSet(imgDataObject, xPos - x, yPos + y, colorObject);
		CGLSet(imgDataObject, xPos - y, yPos - x, colorObject);
		CGLSet(imgDataObject, xPos + x, yPos - y, colorObject);
		CGLSet(imgDataObject, xPos + y, yPos + x, colorObject);
		radius = err;
		if (radius <= y) err = err + ++y * 2 + 1;
		if (radius > x || err > y) err = err + ++x * 2 + 1;
	} while (x < 0);
}





function CGLCircleFilled6Optimized(imgDataObject, x0, y0, radius, colorObject)
{
	// Variation of the midpoint circle algorithm, modified and optimized by Mercury Thirteen
	var x		= radius;
	var y		= 0;
	var err		= 1 - x;

	while (x >= y)
	{
		CGLLineHorizontal(imgDataObject, x0 - y, y0 + x, y << 1, colorObject);
		CGLLineHorizontal(imgDataObject, x0 - y, y0 - x, y << 1, colorObject);
		CGLLineHorizontal(imgDataObject, x0 - x, y0 + y, x << 1, colorObject);
		CGLLineHorizontal(imgDataObject, x0 - x, y0 - y, x << 1, colorObject);

		y++;

		if (err < 0)
		{
			err = err + 1 + (y << 1);
		} else {
			x--;
			err = err + ((y - x + 1) << 1);
		}
	}
}





function CGLCircleFilled5Optimized(imgDataObject, ox, oy, r, colorObject)
{
	// Algorithm from answer submitted by user Daniel Earwicker at
	// https://stackoverflow.com/a/1201227
	// Best: 7.59999990 ms for 1000 iterations

	for (var y = -r; y < r; y++)
	{
		var height = Math.round(Math.sqrt(r * r - y * y));
		CGLLineHorizontal(imgDataObject, ox - height, y + oy, height << 1, colorObject);
	}
}





function CGLCircleFilled4Optimized(imgDataObject, startX, startY, radius, colorObject)
{
	// Algorithm from answer submitted by user palm3D at
	// https://stackoverflow.com/a/1237519
	// Best: 7.59999990 ms for 1000 iterations

	var radiusSquared = radius * radius;
	for(var y = -radius; y < radius; y++)
	{
		var ySquared = y * y;
		for(var x = -radius; x < radius; x++)
		{
			if(x * x + ySquared < radiusSquared)
			{
				CGLLineHorizontal(imgDataObject, startX + x, startY + y, Math.abs(x) << 1, colorObject);
				break;
			}
		}
	}
}





function CGLCircleFilled3Optimized(imgDataObject, x, y, r, colorObject)
{
	// Algorithm from answer submitted by user kmillen at
	// https://stackoverflow.com/a/24453110
	// Best: 7.40000010 ms for 1000 iterations
	for (var cy = -r; cy <= r; cy++)
	{
		var cx = Math.round(Math.sqrt(r * r - cy * cy));

		CGLLineHorizontal(imgDataObject, x - cx, cy + y, cx << 1, colorObject);
	}
}





function CGLCircleFilled(imgDataObject, startX, startY, radius, colorObject)
{
	// This algorithm is an optimized version of that submitted by user Arphimigon at
	// https://stackoverflow.com/a/54864153
	// Best: 7.39999962 ms for 1000 iterations

	var largestX = radius;

	for (var y = 0; y <= radius; ++y)
	{
		for (var x = largestX; x >= 0; --x)
		{
			if ((x * x) + (y * y) < radius * radius)
			{
				CGLLineHorizontal(imgDataObject, startX - x, startY + y, x << 1, colorObject);
				CGLLineHorizontal(imgDataObject, startX - x, startY - y, x << 1, colorObject);

				largestX = x;

				break;
			}
		}
	}
}




function CGLCircleFilled1Optimized(imgDataObject, center_x, center_y, radius, colorObject)
{
	// "DrawCircleBruteforcePrecalcV3" algorithm from answer submitted by user AlexGeorg at
	// https://stackoverflow.com/a/59211338
	// Best: 7.59999990 ms for 1000 iterations

	for (var y = -radius; y < radius; y++)
	{
		var hh = Math.round(Math.sqrt(radius * radius - y * y));

		CGLLineHorizontal(imgDataObject, center_x - hh, center_y + y, hh << 1, colorObject);
	}
}





function CGLCircleFilled5(imgDataObject, ox, oy, r, colorObject)
{
	// Algorithm from answer submitted by user Daniel Earwicker at
	// https://stackoverflow.com/a/1201227
	// Best: 41.79999971 ms for 1000 iterations

	for (var x = -r; x < r; x++)
	{
		var height = Math.round(Math.sqrt(r * r - x * x));
		for (var y = -height; y < height; y++) CGLSet(imgDataObject, x + ox, y + oy, colorObject);
	}
}





function CGLCircleFilled4(imgDataObject, startX, startY, radius, colorObject)
{
	// Algorithm from answer submitted by user palm3D at
	// https://stackoverflow.com/a/1237519
	// Best: 44.40000010 ms for 1000 iterations

	for(var y = -radius; y < radius; y++)
	{
		for(var x = -radius; x < radius; x++)
		{
			if(x * x + y * y < radius * radius) CGLSet(imgDataObject, startX + x, startY + y, colorObject);
		}
	}
}





function CGLCircleFilled3(imgDataObject, x, y, r, colorObject)
{
	// Algorithm from answer submitted by user kmillen at
	// https://stackoverflow.com/a/24453110
	// Best: 75.09999990 ms for 1000 iterations

	var r2 = r * r;
	for (var cy = -r; cy <= r; cy++)
	{
		var cx = Math.round(Math.sqrt(r2 - cy * cy));
		var cyy = cy + y;

		CGLLine(imgDataObject, x - cx, cyy, x + cx, cyy, colorObject);
	}
}





function CGLCircleFilled2(imgDataObject, startX, startY, radius, colorObject)
{
	// Algorithm from answer submitted by user Arphimigon at
	// https://stackoverflow.com/a/54864153
	// Best: 71.59999990 ms for 1000 iterations

	var largestX = radius;
	for (var y = 0; y <= radius; ++y)
	{
		for (var x = largestX; x >= 0; --x)
		{
			if ((x * x) + (y * y) <= (radius * radius))
			{
				CGLLine(imgDataObject, startX - x, startY + y, startX + x, startY + y, colorObject)
				CGLLine(imgDataObject, startX - x, startY - y, startX + x, startY - y, colorObject)

				largestX = x;
				break;		// go to next y coordinate
			}
		}
	}
}





function CGLCircleFilled1(imgDataObject, center_x, center_y, radius, colorObject)
{
	// "DrawCircleBruteforcePrecalcV3" algorithm from answer submitted by user AlexGeorg at
	// https://stackoverflow.com/a/59211338
	// Best: 39.79999971 ms for 1000 iterations
	for (var x = -radius; x < radius; x++)
	{
		var hh = Math.round(Math.sqrt(radius * radius - x * x));
		var rx = center_x + x;
		var ph = center_y + hh;

		for (var y = center_y - hh; y < ph; y++) CGLSet(imgDataObject, rx, y, colorObject);
	}
}





function CGLColorObjectsEqual(colorObjectA, colorObjectB)
{
	var alphaA = 0;
	var alphaB = 0;

	try
	{
		alphaA = colorObjectA.a;
	} catch {
		alphaA = 0xFF;
	}

	try
	{
		alphaB = colorObjectB.a;
	} catch {
		alphaB = 0xFF;
	}

	if
	(
		colorObjectA.r	== colorObjectB.r &&
		colorObjectA.g	== colorObjectB.g &&
		colorObjectA.b	== colorObjectB.b &&
		alphaA			== alphaB
	)
	{
		return true;
	} else {
		return false;
	}
}





function CGLEllipse(imgDataObject, xPos, yPos, xRadius, yRadius, colorObject)
{
	// https://github.com/zingl/Bresenham/blob/master/bresenham.js
	var x = -xRadius
	var y = 0;
	var dx = (1 + 2 * x) * yRadius * yRadius;
	var e2
	var dy = x * x;
	var err = dx + dy;

	do {
		CGLSet(imgDataObject, xPos - x, yPos - y, colorObject);
		CGLSet(imgDataObject, xPos - x, yPos + y, colorObject);
		CGLSet(imgDataObject, xPos + x, yPos - y, colorObject);
		CGLSet(imgDataObject, xPos + x, yPos + y, colorObject);
		e2 = 2 * err;                                        
		if (e2 >= dx) { x++; err += dx += 2 * yRadius * yRadius ; }
		if (e2 <= dy) { y++; err += dy += 2 * xRadius * xRadius ; }
	} while (x <= 0);

	while (y++ < yRadius)
	{
		CGLSet(imgDataObject, xPos, yPos + y, colorObject);
		CGLSet(imgDataObject, xPos, yPos - y, colorObject);
	}
}





function CGLEllipseFilled(imgDataObject, xPos, yPos, xRadius, yRadius, colorObject)
{
	// https://github.com/zingl/Bresenham/blob/master/bresenham.js
	var x = -xRadius
	var y = 0;
	var dx = (1 + 2 * x) * yRadius * yRadius;
	var e2
	var dy = x * x;
	var err = dx + dy;

	do {
		var height = y * 2;
		CGLLineVertical(imgDataObject, xPos - x, yPos - y, height, colorObject);
		CGLLineVertical(imgDataObject, xPos + x, yPos - y, height, colorObject);

		e2 = 2 * err;

		if (e2 >= dx)
		{
			x++;
			err += dx += 2 * yRadius * yRadius;
		}

		if (e2 <= dy)
		{
			y++;
			err += dy += 2 * xRadius * xRadius;
		}

	} while (x <= 0);

	while (y++ < yRadius)
	{
		CGLLineVertical(imgDataObject, xPos, yPos - y, y * 2, colorObject);
	}
}





function CGLFill(imgDataObject, xPosition, yPosition, colorObject)
{
	// https://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= imgDataObject.width * bytesPerPixel;
	var		startColor		= CGLGet(imgDataObject, xPosition, yPosition);
	var		xStack			= [];
	var		yStack			= [];
	var		alpha			= 0;

	if (CGLColorObjectsEqual(startColor, colorObject)) return;
	if (xPosition >= imgDataObject.width || yPosition >= imgDataObject.height) return;

	xStack.push(xPosition);
	yStack.push(yPosition);

	alpha = colorObject.a;
	if (alpha === undefined) alpha = 0xFF;

	while (xStack.length)
	{
		var x, y, pixelPos, reachLeft, reachRight;
		x = xStack.pop();
		y = yStack.pop();

		pixelPos = (y * bytesPerLine) + (x * bytesPerPixel);
		while (y-- >= 0 && CGLFill_MatchStartColor(imgDataObject, pixelPos, startColor))
		{
			pixelPos = pixelPos - bytesPerLine;
		}

		y++;
		pixelPos	= pixelPos + bytesPerLine;
		reachLeft	= false;
		reachRight	= false;

		while (y++ <= imgDataObject.height && CGLFill_MatchStartColor(imgDataObject, pixelPos, startColor))
		{
			// this is faster than calling out to CGLSet()
			imgDataObject.data[pixelPos]		= colorObject.r;
			imgDataObject.data[pixelPos + 1]	= colorObject.g;
			imgDataObject.data[pixelPos + 2]	= colorObject.b;
			imgDataObject.data[pixelPos + 3]	= alpha;

			if (x > 0)
			{
				if (CGLFill_MatchStartColor(imgDataObject, pixelPos - bytesPerPixel, startColor))
				{
					if (!reachLeft)
					{
						xStack.push(x - 1);
						yStack.push(y);
						reachLeft = true;
					}
				} else if (reachLeft) {
					reachLeft = false;
				}
			}

			if (x <= imgDataObject.width)
			{
				if (CGLFill_MatchStartColor(imgDataObject, pixelPos + bytesPerPixel, startColor))
				{
					if (!reachRight)
					{
						xStack.push(x + 1);
						yStack.push(y);
						reachRight = true;
					}
				} else if(reachRight) {
					reachRight = false;
				}
			}

			pixelPos = pixelPos + bytesPerLine;
		}
	}
}





function CGLFill_MatchStartColor(imgDataObject, pixelPos, startColor)
{
	var r = imgDataObject.data[pixelPos];	
	var g = imgDataObject.data[pixelPos + 1];	
	var b = imgDataObject.data[pixelPos + 2];

	return (r == startColor.r && g == startColor.g && b == startColor.b);
}





function CGLGet(imgDataObject, x, y)
{
	arrayIndex = (y * imgDataObject.width + x) * 4;

	var colorObject	=
	{
		r: imgDataObject.data[arrayIndex],
		g: imgDataObject.data[arrayIndex + 1],
		b: imgDataObject.data[arrayIndex + 2],
		a: imgDataObject.data[arrayIndex + 3]
	};

	return colorObject;
}





function CGLLine(imgDataObject, x0, y0, x1, y1, colorObject)
{
	// Time for some good ol' fashioned Bresenhamation!
	// https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
	deltaX	= Math.abs(x1 - x0);
	sx		= x0 < x1 ? 1 : -1;
	deltaY	= -Math.abs(y1 - y0);
	sy		= y0 < y1 ? 1 : -1;
	error	= deltaX + deltaY;

	while (true)
	{
		CGLSet(imgDataObject, x0, y0, colorObject);
		if (x0 == x1 && y0 == y1) break;

		e2 = 2 * error;

		if (e2 >= deltaY)
		{
			error = error + deltaY;
			x0 = x0 + sx;
		}

		if (e2 <= deltaX)
		{
			error = error + deltaX;
			y0 = y0 + sy;
		}
	}
}





function CGLLineDiagonalNE(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= (imgDataObject.width - 1) * bytesPerPixel;

	if (x < 0)
	{
		length = length + x;
		y = y + x;
		x = 0;
	}
	if (x + length - 1 >= imgDataObject.width) length = length - ((x + length) - imgDataObject.width);

	if (y < 0)
	{
		length = length + y;
		y = 0;
	}

	if (y >= imgDataObject.height)
	{
		if (y - length + 1 >= imgDataObject.height)
		{
			return;
		} else {
			var diff = y - imgDataObject.height;
			length = length - diff;
			y = y - diff;
			x = x + diff;
		}
	}

	var pixelOffset		= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha			= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{

		imgDataObject.data[pixelOffset]		= colorObject.r;
		imgDataObject.data[pixelOffset + 1]	= colorObject.g;
		imgDataObject.data[pixelOffset + 2]	= colorObject.b;
		imgDataObject.data[pixelOffset + 3]	= alpha;
		pixelOffset = pixelOffset - bytesPerLine;
	}
}





function CGLLineDiagonalNW(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= (imgDataObject.width + 1) * bytesPerPixel;

	if (x < 0)
	{
		length = length + x;
		x = 0;
	}
	if (x + length - 1 >= imgDataObject.width) length = length - ((x + length) - imgDataObject.width);

	if (y < 0)
	{
		length = length + y;
		y = 0;
	}
	if (y + length - 1 >= imgDataObject.height) length = length - ((y + length) - imgDataObject.height);

	var pixelOffset		= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha			= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{
		imgDataObject.data[pixelOffset]		= colorObject.r;
		imgDataObject.data[pixelOffset + 1]	= colorObject.g;
		imgDataObject.data[pixelOffset + 2]	= colorObject.b;
		imgDataObject.data[pixelOffset + 3]	= alpha;
		pixelOffset = pixelOffset - bytesPerLine;
	}
}





function CGLLineDiagonalSE(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= (imgDataObject.width + 1) * bytesPerPixel;

	if (x < 0)
	{
		length = length + x;
		x = 0;
	}
	if (x + length - 1 >= imgDataObject.width) length = length - ((x + length) - imgDataObject.width);

	if (y < 0)
	{
		length = length + y;
		y = 0;
	}
	if (y + length - 1 >= imgDataObject.height) length = length - ((y + length) - imgDataObject.height);

	var pixelOffset		= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha			= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{
		imgDataObject.data[pixelOffset]		= colorObject.r;
		imgDataObject.data[pixelOffset + 1]	= colorObject.g;
		imgDataObject.data[pixelOffset + 2]	= colorObject.b;
		imgDataObject.data[pixelOffset + 3]	= alpha;
		pixelOffset = pixelOffset + bytesPerLine;
	}
}





function CGLLineDiagonalSW(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= (imgDataObject.width - 1) * bytesPerPixel;

	if (x - length + 1 < 0)
	{
		if (x < 0)
		{
			return;
		} else {
			length = length + (x - length + 1);
		}
	}
	if (x >= imgDataObject.width)
	{
		if (x - length + 1 >= imgDataObject.width)
		{
			return;
		} else {
			var diff = x - imgDataObject.width;
			length = length - diff;
			x = x - diff;
			y = y + diff;
		}
	}

	if (y < 0)
	{
		length = length + y;
		x = x + y;
		y = 0;
	}
	if (y + length - 1 >= imgDataObject.height) length = length - ((y + length) - imgDataObject.height);

	var pixelOffset		= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha			= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{
		imgDataObject.data[pixelOffset]		= colorObject.r;
		imgDataObject.data[pixelOffset + 1]	= colorObject.g;
		imgDataObject.data[pixelOffset + 2]	= colorObject.b;
		imgDataObject.data[pixelOffset + 3]	= alpha;
		pixelOffset = pixelOffset + bytesPerLine;
	}
}





function CGLLineHorizontal(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;

	if (y < 0 || y >= imgDataObject.height)	return;

	if (x < 0)
	{
		length = length + x;
		x = 0;
	}
	if (x + length - 1 >= imgDataObject.width) length = length - ((x + length) - imgDataObject.width);

	var pixelOffset	= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha		= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{
		imgDataObject.data[pixelOffset++] = colorObject.r;
		imgDataObject.data[pixelOffset++] = colorObject.g;
		imgDataObject.data[pixelOffset++] = colorObject.b;
		imgDataObject.data[pixelOffset++] = alpha;
	}
}





function CGLLineVertical(imgDataObject, x, y, length, colorObject)
{
	const	bytesPerPixel	= 4;
	const	bytesPerLine	= imgDataObject.width * bytesPerPixel;

	if (x < 0 || x >= imgDataObject.width)	return;

	if (y < 0)
	{
		length = length + y;
		y = 0;
	}
	if (y + length - 1 >= imgDataObject.height) length = length - ((y + length) - imgDataObject.height);

	var pixelOffset		= (y * imgDataObject.width + x) * bytesPerPixel;
	var alpha			= colorObject.a;

	if (alpha === undefined) alpha = 0xFF;

	for (var i = 0; i < length; i++)
	{
		imgDataObject.data[pixelOffset]		= colorObject.r;
		imgDataObject.data[pixelOffset + 1]	= colorObject.g;
		imgDataObject.data[pixelOffset + 2]	= colorObject.b;
		imgDataObject.data[pixelOffset + 3]	= alpha;
		pixelOffset = pixelOffset + bytesPerLine;
	}
}





function CGLRect(imgDataObject, x0, y0, width, height, colorObject)
{
	var x1 = x0 + width - 1;
	var y1 = y0 + height - 1;

	// There's room for optimization here...
	for (var x = x0; x <= x1; x++)
	{
		CGLSet(imgDataObject, x, y0, colorObject);
		CGLSet(imgDataObject, x, y1, colorObject);
	}

	for (var y = y0; y <= y1; y++)
	{
		CGLSet(imgDataObject, x0, y, colorObject);
		CGLSet(imgDataObject, x1, y, colorObject);
	}
}





function CGLRectFilled(imgDataObject, x, y, width, height, colorObject)
{
	var yEnd = y + height;

	for (var i = y; i < yEnd; i++)
	{
		CGLLineHorizontal(imgDataObject, x, i, width, colorObject);
	}
}





function CGLRoundRect(imgDataObject, x, y, w, h, r, colorObject)
{
	const radiusX2 = r * 2;

	var max_radius = ((w < h) ? w : h) / 2; // 1/2 minor axis
	if (r > max_radius) r = max_radius;

	CGLLineHorizontal(imgDataObject,	x + r,				y,					w - radiusX2, colorObject);
	CGLLineHorizontal(imgDataObject,	x + r,				y + h - 1,			w - radiusX2, colorObject);
	CGLLineVertical(imgDataObject,		x,					y + r,				h - radiusX2, colorObject);
	CGLLineVertical(imgDataObject,		x + w - 1,			y + r,				h - radiusX2, colorObject);

	CGL_CircleHelper(imgDataObject,		x + r,				y + r,				r, 1, colorObject);
	CGL_CircleHelper(imgDataObject,		x + w - r - 1,		y + r,				r, 2, colorObject);
	CGL_CircleHelper(imgDataObject,		x + w - r - 1,		y + h - r - 1,		r, 4, colorObject);
	CGL_CircleHelper(imgDataObject,		x + r,				y + h - r - 1,		r, 8, colorObject);
}





function CGLRoundRectFilled(imgDataObject, x, y, w, h, r, colorObject)
{
	var max_radius = ((w < h) ? w : h) / 2; // 1/2 minor axis
	if (r > max_radius) r = max_radius;

	CGLRectFilled(imgDataObject,			x + r,				y,			w - 2 * r, 		h,				colorObject);
	CGL_CircleFilledHelper(imgDataObject,	x + w - r - 1,		y + r,		r,		1, 		h - 2 * r - 1,	colorObject);
	CGL_CircleFilledHelper(imgDataObject,	x + r,				y + r,		r,		2, 		h - 2 * r - 1,	colorObject);
}





function CGLSet(imgDataObject, x, y, colorObject)
{
	var alpha;
	if (colorObject.a === undefined) {alpha = 0xFF} else {alpha = colorObject.a};
	if (x < 0 || x >= imgDataObject.width)	return;
	if (y < 0 || y >= imgDataObject.height)	return;

	if (alpha != 0x00)
	{
		arrayIndex = ((y * imgDataObject.width) + x) * 4;
		imgDataObject.data[arrayIndex]		= colorObject.r;
		imgDataObject.data[arrayIndex + 1]	= colorObject.g;
		imgDataObject.data[arrayIndex + 2]	= colorObject.b;
		imgDataObject.data[arrayIndex + 3]	= alpha;
	}
}





function CGLTextChar(imgDataObject, x, y, charCode, fontObject, textColor, backgroundColor)
{
	var xRightStart = fontObject.width - 1;
	for (var yOffset = 0; yOffset < fontObject.height; yOffset++)
	{
		for (var xOffset = 0; xOffset < fontObject.width; xOffset++)
		{
			if (fontObject.data[charCode][yOffset] & (1 << xOffset))
			{
				CGLSet(imgDataObject, x + xRightStart - xOffset, y + yOffset, textColor);
			} else {
				CGLSet(imgDataObject, x + xRightStart - xOffset, y + yOffset, backgroundColor);
			}
		}
	}
}





function CGLTextString(imgDataObject, x, y, string, fontObject, textColor, backgroundColor)
{
	for (var i = 0; i < string.length; i++)
	{
		CGLTextChar(imgDataObject, x + (i * fontObject.width), y, string.charCodeAt(i), fontObject, textColor, backgroundColor);
	}
}







function CGLTriangle(imgDataObject, x0, y0, x1, y1, x2, y2, colorObject)
{
	CGLLine(imgDataObject, x0, y0, x1, y1, colorObject);
	CGLLine(imgDataObject, x1, y1, x2, y2, colorObject);
	CGLLine(imgDataObject, x2, y2, x0, y0, colorObject);
}





function CGLTriangleFilled(imgDataObject, x0, y0, x1, y1, x2, y2, colorObject)
{
	/*
		Draws a triangle at the points specified.
		Adapted from code found at https://gabrielgambetta.com/computer-graphics-from-scratch/demos/raster-03.html
		Modified by Mercury Thriteen to use whole-line drawing, resulting in more than a 5x speedup
	*/

	// Sort the points so that y0 <= y1 <= y2
	if (y1 < y0) {var swap = x0; x0 = x1; x1 = swap; swap = y0; y0 = y1; y1 = swap;}
	if (y2 < y0) {var swap = x0; x0 = x2; x2 = swap; swap = y0; y0 = y2; y2 = swap;}
	if (y2 < y1) {var swap = x1; x1 = x2; x2 = swap; swap = y1; y1 = y2; y2 = swap;}

	// Compute the x coordinates of the triangle edges
	var x01 = CGL_Interpolate(y0, x0, y1, x1);
	var x12 = CGL_Interpolate(y1, x1, y2, x2);
	var x02 = CGL_Interpolate(y0, x0, y2, x2);

	// Concatenate the short sides
	x01.pop();
	var x012 = x01.concat(x12);

	// Determine which is left and which is right
	var m = (x012.length / 2) | 0;
	if (x02[m] < x012[m])
	{
		x_left	= x02;
		x_right	= x012;
	} else {
		x_left	= x012;
		x_right	= x02;
	}

	for (var i = 0; i <= y2 - y0; i++)
	{
		CGLLineHorizontal(imgDataObject, x_left[i], i + y0, x_right[i] - x_left[i], colorObject);
	}
}
