/* 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); } }