UNPKG

@aurigma/design-atoms

Version:

Design Atoms is a part of Customer's Canvas SDK which allows for manipulating individual design elements through your code.

485 lines 22.5 kB
// ReSharper disable once InconsistentNaming import { PointF, EqualsOfFloatNumbers, ConvertDegreeToRadian, RotatedRectangleF, Path, Transform, SizeF } from "@aurigma/design-atoms-model/Math"; import { NotImplementedException } from "@aurigma/design-atoms-model/Exception"; import { Margin } from "@aurigma/design-atoms-model/Math/Margin"; export class Graphics { static drawCross(ctx, x, y, size, width, baselineColor) { const rect = new RotatedRectangleF(x, y, size, size).toRectangleF(); Graphics.drawLine(ctx, rect.left, rect.top, rect.right, rect.bottom, width, baselineColor); Graphics.drawLine(ctx, rect.left, rect.bottom, rect.right, rect.top, width, baselineColor); } static drawLine(ctx, x, y, x1, y1, lineWidth, lineColor, opacity = 1, dashWidth = null) { if (Graphics.isFullTransparentColor(lineColor) || lineWidth <= 0 || opacity == null || opacity <= 0) return; ctx.save(); try { if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; ctx.lineWidth = lineWidth; ctx.strokeStyle = lineColor; if (dashWidth) ctx.setLineDash(dashWidth); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x1, y1); ctx.stroke(); } finally { ctx.restore(); } } static drawDashedLine(ctx, x0, y0, x1, y1, lineWidth, color, altColor, dashWidth, altDashWidth, opacity = 1) { if (lineWidth > 0 && dashWidth > 0 && altDashWidth > 0 && (opacity == null || opacity > 0)) { ctx.save(); if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; ctx.lineWidth = lineWidth; if (x1 < x0) { var t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; } let dx = x1 - x0; let dy = y1 - y0; var d = dashWidth + altDashWidth; var len = Math.sqrt(dx * dx + dy * dy); var dashCount = Math.floor(len / d); dx /= (len / d); dy /= (len / d); var dashX = dx * (dashWidth / d); var altDashX = dx * (altDashWidth / d); var dashY = dy * (dashWidth / d); var altDashY = dy * (altDashWidth / d); var x = x0; var y = y0; var i; if (!Graphics.isFullTransparentColor(color)) { ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo(x, y); for (i = 0; i < dashCount; i++) { x += dashX; y += dashY; ctx.lineTo(x, y); x += altDashX; y += altDashY; if (i + 1 < dashCount) { ctx.moveTo(x, y); } } if (x + dashX <= x1 && y + dashY <= y1) { ctx.moveTo(x, y); ctx.lineTo(x + dashX, y + dashY); } else { ctx.moveTo(x, y); ctx.lineTo(x1, y1); } ctx.closePath(); ctx.stroke(); } x = x0 + dashX; y = y0 + dashY; if (!Graphics.isFullTransparentColor(altColor)) { ctx.strokeStyle = altColor; ctx.beginPath(); ctx.moveTo(x, y); for (i = 0; i < dashCount; i++) { x += altDashX; y += altDashY; ctx.lineTo(x, y); x += dashX; y += dashY; if (i + 1 < dashCount) { ctx.moveTo(x, y); } } if (x < x1 && y < y1) { ctx.moveTo(x, y); ctx.lineTo(x1, y1); } ctx.closePath(); ctx.stroke(); } ctx.restore(); } } static drawPolyline(ctx, points, lineWidth, lineColor, opacity) { if (!Graphics.isFullTransparentColor(lineColor) && lineWidth > 0 && (opacity == null || opacity > 0)) { if (points && points.length > 1) { ctx.save(); if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; ctx.lineWidth = lineWidth; ctx.strokeStyle = lineColor; ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1, imax = points.length; i < imax; ++i) { const p = points[i]; ctx.lineTo(p.x, p.y); } ctx.stroke(); ctx.restore(); } } } static drawPath(ctx, path, center, transform, borderWidth, borderColor, opacity, dashWidth = [], altBorderColor = null) { Graphics.path(ctx, path, center, transform, null, borderWidth, borderColor, opacity, dashWidth, altBorderColor); } static fillPath(ctx, path, center, transform, fillColor, opacity) { Graphics.path(ctx, path, center, transform, fillColor, 0, null, opacity, []); } static drawStroke(ctx, path, center, transform, borderWidth, borderColor, altBorderColor, opacity, dash) { if (borderWidth <= 0) return; Graphics.path(ctx, path, center, transform, null, borderWidth, borderColor, opacity, dash); if (altBorderColor != null && dash != null && dash.length > 0) { var altDash = dash.slice(0); altDash.unshift(0); altDash.push(0); Graphics.path(ctx, path, center, transform, null, borderWidth, altBorderColor, opacity, altDash); } } static path(ctx, path, center, transform, fillColor, borderWidth, borderColor, opacity, dashWidth, altBorderColor = null) { const stroke = borderColor != null && !Graphics.isFullTransparentColor(borderColor) && borderWidth > 0; const fill = fillColor != null && !Graphics.isFullTransparentColor(fillColor); if ((stroke || fill) && (opacity == null || opacity > 0)) { ctx.save(); if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; var matrix = transform.toMatrix(); ctx.translate(center.x, center.y); ctx.transform(matrix.m00, matrix.m10, matrix.m01, matrix.m11, matrix.m02, matrix.m12); ctx.translate(-center.x, -center.y); if (path instanceof Path) path.draw(ctx); else if (typeof path.addToContext == "function") //TextWhizz.Path path.addToContext(ctx); if (fill) { ctx.fillStyle = fillColor; ctx.fill(); } if (stroke) { if (path instanceof Path && !EqualsOfFloatNumbers(transform.scaleX, transform.scaleY)) { ctx.restore(); ctx.save(); if (opacity) ctx.globalAlpha = opacity; var transformedPath = path.clone(); transformedPath.transform(transform, center); transformedPath.draw(ctx); } else { borderWidth /= transform.scaleX; if (dashWidth && dashWidth.length > 0) { for (let i = 0; i < dashWidth.length; i++) dashWidth[i] /= transform.scaleX; } } ctx.lineWidth = borderWidth; ctx.strokeStyle = borderColor; ctx.setLineDash(dashWidth); ctx.stroke(); if (dashWidth != null && dashWidth.length > 0 && altBorderColor) { const altDash = [0, ...dashWidth, 0]; ctx.setLineDash(altDash); ctx.strokeStyle = altBorderColor; ctx.stroke(); } } ctx.restore(); } } static clipPath(ctx, path) { path.draw(ctx); ctx.clip(); } static drawImage(ctx, image, rotatedRectangle, scaleX, scaleY, disableSmoothing = false, maskColor = null, opacity = 1, imageRect = null, increaseImageRect = 0) { if (opacity == null || opacity > 0) { ctx.save(); if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; ctx.translate(rotatedRectangle.centerX, rotatedRectangle.centerY); ctx.rotate(ConvertDegreeToRadian(rotatedRectangle.angle)); if (increaseImageRect) { const mx = increaseImageRect / scaleX; const my = increaseImageRect / scaleY; rotatedRectangle.width += mx * 2; rotatedRectangle.height += my * 2; } let imageWidth, imageHeight; if (imageRect != null) { imageWidth = imageRect.width; imageHeight = imageRect.height; } else { if (image.naturalWidth) imageWidth = image.naturalWidth; else if (image.width) imageWidth = image.width; else imageWidth = ctx.canvas.width; if (image.naturalHeight) imageHeight = image.naturalHeight; else if (image.height) imageHeight = image.height; else imageHeight = ctx.canvas.height; } const drawImageAsIs = scaleX != undefined && scaleX !== 0 && scaleY != undefined && scaleY !== 0 && Math.abs(imageWidth - rotatedRectangle.width * scaleX) < 1 && Math.abs(imageHeight - rotatedRectangle.height * scaleY) < 1; ; if (!disableSmoothing) { // depends on angle if parameter not specified disableSmoothing = (ctx.imageSmoothingEnabled || ctx.msImageSmoothingEnabled === true) && Math.abs(rotatedRectangle.angle) % 90 < 0.01 && drawImageAsIs; } if (disableSmoothing) { ctx.imageSmoothingEnabled = false; ctx.msImageSmoothingEnabled = false; } try { if (drawImageAsIs) { ctx.scale(1 / scaleX, 1 / scaleY); if (imageRect != null) ctx.drawImage(image, imageRect.left, imageRect.top, imageRect.width, imageRect.height, -rotatedRectangle.width * scaleX / 2, -rotatedRectangle.height * scaleY / 2, imageWidth, imageHeight); else ctx.drawImage(image, -rotatedRectangle.width * scaleX / 2, -rotatedRectangle.height * scaleY / 2, imageWidth, imageHeight); ctx.scale(scaleX, scaleY); } else if (imageRect != null) ctx.drawImage(image, imageRect.left, imageRect.top, imageRect.width, imageRect.height, -rotatedRectangle.width / 2, -rotatedRectangle.height / 2, rotatedRectangle.width, rotatedRectangle.height); else ctx.drawImage(image, -rotatedRectangle.width / 2, -rotatedRectangle.height / 2, rotatedRectangle.width, rotatedRectangle.height); if (maskColor && !Graphics.isFullTransparentColor(maskColor)) { ctx.fillStyle = maskColor; ctx.fillRect(-rotatedRectangle.width / 2, -rotatedRectangle.height / 2, rotatedRectangle.width, rotatedRectangle.height); } } catch (err) { } if (disableSmoothing) { ctx.imageSmoothingEnabled = true; ctx.msImageSmoothingEnabled = true; } ctx.restore(); } } static drawRectangle(ctx, rotatedRectangle, borderWidth, borderColor, opacity) { Graphics.rectangle(ctx, rotatedRectangle, borderWidth, borderColor, null, opacity); } static fillRectangle(ctx, rotatedRectangle, fillColor, opacity) { Graphics.rectangle(ctx, rotatedRectangle, 0, null, fillColor, opacity); } static rectangle(ctx, rotatedRectangle, borderWidth, borderColor, fillColor = null, opacity = 1, strokeDash = null) { var stroke = borderColor && !Graphics.isFullTransparentColor(borderColor) && borderWidth > 0; var fill = fillColor && !Graphics.isFullTransparentColor(fillColor); if ((stroke || fill) && (opacity == null || opacity > 0)) { ctx.save(); if (opacity) ctx.globalAlpha = ctx.globalAlpha * opacity; ctx.translate(rotatedRectangle.centerX, rotatedRectangle.centerY); ctx.rotate(ConvertDegreeToRadian(rotatedRectangle.angle)); if (fill) { ctx.fillStyle = fillColor; ctx.fillRect(-rotatedRectangle.width / 2, -rotatedRectangle.height / 2, rotatedRectangle.width, rotatedRectangle.height); } if (stroke) { if (strokeDash != null) ctx.setLineDash(strokeDash); ctx.lineWidth = borderWidth; ctx.strokeStyle = borderColor; ctx.strokeRect(-rotatedRectangle.width / 2, -rotatedRectangle.height / 2, rotatedRectangle.width, rotatedRectangle.height); } ctx.restore(); } } static measureText(ctx, text, point, font, fillColor = null, strokeColor = null, angle = 0) { var stroke = strokeColor && !Graphics.isFullTransparentColor(strokeColor); var fill = fillColor && !Graphics.isFullTransparentColor(fillColor); if (stroke == null && fill == null) return null; ctx.save(); try { ctx.font = font; ctx.translate(point.x, point.y); ctx.rotate(ConvertDegreeToRadian(angle)); if (fill) ctx.fillStyle = fillColor; if (stroke) ctx.strokeStyle = strokeColor; return ctx.measureText(text); } finally { ctx.restore(); } } static _drawTextBackground(ctx, left, right, top, bottom, background, border, borderWidth, margin, cornerRadius) { const width = right - left; const height = bottom - top; let radiuses = null; if (cornerRadius > 0) { const cornerSize = new SizeF(cornerRadius, cornerRadius); radiuses = [cornerSize, cornerSize, cornerSize, cornerSize]; } const rectPath = Path.roundedRectangle(left - margin.left, top - margin.top, width + margin.left + margin.right, height + margin.top + margin.bottom, radiuses); Graphics.path(ctx, rectPath, new PointF(), new Transform(), background, borderWidth, border, 1, []); } static text(ctx, text, point, font, fillColor = null, strokeColor = null, angle = 0, maxWidth = null, textAlign = "left", textBaseline = "alphabetic", background = null, border = null, borderWidth = 0, backgroundMargin = new Margin(0), backgroundCornerRadius = 0) { if (background != null && (textBaseline != "bottom" && textBaseline != "middle")) throw new NotImplementedException(`textBaseline = ${textBaseline} is not supported yet`); const stroke = strokeColor && !Graphics.isFullTransparentColor(strokeColor); const fill = fillColor && !Graphics.isFullTransparentColor(fillColor); if (stroke || fill) { ctx.save(); ctx.font = font; ctx.translate(point.x, point.y); ctx.rotate(ConvertDegreeToRadian(angle)); ctx.textAlign = textAlign; ctx.textBaseline = textBaseline; if (fill) ctx.fillStyle = fillColor; if (stroke) ctx.strokeStyle = strokeColor; const maxWidthInChars = Math.floor(maxWidth / ctx.measureText("x").width); // x is average width char const lines = this._splitTextToLines(text, maxWidthInChars); if (lines.length > 1 && textBaseline != "bottom") throw new NotImplementedException(`textBaseline = ${textBaseline} is not supported yet`); let widestMetrics = ctx.measureText(""); let actualBoundingBoxAscent = 0; let actualBoundingBoxDescent = 0; lines.forEach((a) => { const metrics = ctx.measureText(a); actualBoundingBoxAscent += metrics.actualBoundingBoxAscent; actualBoundingBoxDescent += metrics.actualBoundingBoxDescent; if (metrics.width > widestMetrics.width) widestMetrics = metrics; }); const fontSizeStr = font.match(/\d+(px|pt)/); let pxFontSize = 0; if (fontSizeStr.length > 0) { const val = Number.parseFloat(fontSizeStr[0].substring(0, fontSizeStr[0].length - 2)); if (fontSizeStr[1] == "px") pxFontSize = val; else pxFontSize = val * 1.33; } const fontSize = pxFontSize > 0 ? pxFontSize : widestMetrics.actualBoundingBoxAscent + widestMetrics.actualBoundingBoxDescent; const pxBetweenLines = (fontSize) * 10 / 7; const height = fontSize + pxBetweenLines * (lines.length - 1); let textYStart = 0; switch (textBaseline) { //TODO: other cases case "bottom": actualBoundingBoxDescent = -pxBetweenLines / 40; actualBoundingBoxAscent = height - widestMetrics.actualBoundingBoxDescent; textYStart = -pxBetweenLines * (lines.length - 1); break; default: break; } if (background || (border != null && borderWidth > 0)) { this._drawTextBackground(ctx, -widestMetrics.actualBoundingBoxLeft, widestMetrics.actualBoundingBoxRight, -actualBoundingBoxAscent, actualBoundingBoxDescent, background, border, borderWidth, backgroundMargin, backgroundCornerRadius); } lines.forEach((a, i) => { if (fill) ctx.fillText(a, 0, textYStart + i * (pxBetweenLines)); if (stroke) ctx.strokeText(a, 0, textYStart + i * (pxBetweenLines)); }); ctx.restore(); } } static _splitTextToLines(text, maxWidth = null) { let lines = null; if (maxWidth) { const reg = new RegExp(`(?![^\\n]{1,${maxWidth}}$)([^\\n]{1,${maxWidth}})\\s`, 'g'); const cropedText = text.replace(reg, '$1\n'); // do linebreaks at nearest to Whitespace places lines = cropedText.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].length > maxWidth) { const newLine = lines[i].substring(maxWidth); lines[i] = lines[i].substring(0, maxWidth); lines.splice(i + 1, 0, newLine); } } } else { lines = text.split("\n"); } return lines; } static isFullTransparentColor(color) { if (!color) return false; //test if color has A, R, G, B properties let a = color.A || color.a; if (a === 0) return true; if (typeof color === "string") { //parse string const rgba = /^\s*rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\,\s*(\d{1,}(\.\d{1,})?)\s*\)\s*;{0,1}\s*$/i; a = rgba.exec(color); if (a) { a = parseFloat(a[1]); if (a === 0) return true; else return false; } } return false; } static clearCanvas(context, preserveTransform = false) { if (preserveTransform) { context.save(); context.setTransform(1, 0, 0, 1, 0, 0); } context.clearRect(0, 0, context.canvas.width, context.canvas.height); if (preserveTransform) { context.restore(); } } static drawRoundedRectangle(ctx, rect, rounded) { ctx.roundedRectangle(rect.left, rect.top, rect.width, rect.height, rounded); } } CanvasRenderingContext2D.prototype.roundedRectangle = function (x, y, width, height, rounded) { const halfRadians = (2 * Math.PI) / 2; const quarterRadians = (2 * Math.PI) / 4; this.beginPath(); // top left arc this.arc(rounded + x, rounded + y, rounded, -quarterRadians, halfRadians, true); // line from top left to bottom left this.lineTo(x, y + height - rounded); // bottom left arc this.arc(rounded + x, height - rounded + y, rounded, halfRadians, quarterRadians, true); // line from bottom left to bottom right this.lineTo(x + width - rounded, y + height); // bottom right arc this.arc(x + width - rounded, y + height - rounded, rounded, quarterRadians, 0, true); // line from bottom right to top right this.lineTo(x + width, y + rounded); // top right arc this.arc(x + width - rounded, y + rounded, rounded, 0, -quarterRadians, true); // line from top right to top left this.lineTo(x + rounded, y); this.stroke(); this.fill(); this.closePath(); }; //# sourceMappingURL=Graphics.js.map