diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index ad7ada4..0d87451 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -232,6 +232,125 @@ } /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) + / (4.0 * a32); + } + + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate number of segments for the curve based on its length to ensure its smoothness. + * + * @private + * @param {number} length - length of curve + * @return {number} Number of segments + */ + _segmentsCount(length) + { + let result = Math.ceil(length / Graphics.CURVES.maxLength); + + if (result < Graphics.CURVES.minSegments) + { + result = Graphics.CURVES.minSegments; + } + else if (result > Graphics.CURVES.maxSegments) + { + result = Graphics.CURVES.maxSegments; + } + + return result; + } + + /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * @@ -329,7 +448,6 @@ this.moveTo(0, 0); } - const n = 20; const points = this.currentPath.shape.points; let xa = 0; let ya = 0; @@ -341,6 +459,9 @@ const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; + const n = Graphics.CURVES.adaptive + ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) + : 20; for (let i = 1; i <= n; ++i) { @@ -390,7 +511,11 @@ points.length -= 2; - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, points); + const n = Graphics.CURVES.adaptive + ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) + : 20; + + bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); this.dirty++; @@ -496,7 +621,9 @@ } const sweep = endAngle - startAngle; - const segs = Math.ceil(Math.abs(sweep) / PI_2) * 40; + const segs = Graphics.CURVES.adaptive + ? this._segmentsCount(Math.abs(sweep) * radius) + : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -1165,3 +1292,25 @@ } Graphics._SPRITE_TEXTURE = null; + +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI.Graphics + * @name CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +Graphics.CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, +}; diff --git a/packages/graphics/src/Graphics.js b/packages/graphics/src/Graphics.js index ad7ada4..0d87451 100644 --- a/packages/graphics/src/Graphics.js +++ b/packages/graphics/src/Graphics.js @@ -232,6 +232,125 @@ } /** + * Calculate length of quadratic curve + * @see {@link http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/} + * for the detailed explanation of math behind this. + * + * @private + * @param {number} fromX - x-coordinate of curve start point + * @param {number} fromY - y-coordinate of curve start point + * @param {number} cpX - x-coordinate of curve control point + * @param {number} cpY - y-coordinate of curve control point + * @param {number} toX - x-coordinate of curve end point + * @param {number} toY - y-coordinate of curve end point + * @return {number} Length of quadratic curve + */ + _quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY) + { + const ax = fromX - ((2.0 * cpX) + toX); + const ay = fromY - ((2.0 * cpY) + toY); + const bx = 2.0 * ((cpX - 2.0) * fromX); + const by = 2.0 * ((cpY - 2.0) * fromY); + const a = 4.0 * ((ax * ax) + (ay * ay)); + const b = 4.0 * ((ax * bx) + (ay * by)); + const c = (bx * bx) + (by * by); + + const s = 2.0 * Math.sqrt(a + b + c); + const a2 = Math.sqrt(a); + const a32 = 2.0 * a * a2; + const c2 = 2.0 * Math.sqrt(c); + const ba = b / a2; + + return ( + (a32 * s) + + (a2 * b * (s - c2)) + + ( + ((4.0 * c * a) - (b * b)) + * Math.log(((2.0 * a2) + ba + s) / (ba + c2)) + ) + ) + / (4.0 * a32); + } + + /** + * Calculate length of bezier curve. + * Analytical solution is impossible, since it involves an integral that does not integrate in general. + * Therefore numerical solution is used. + * + * @private + * @param {number} fromX - Starting point x + * @param {number} fromY - Starting point y + * @param {number} cpX - Control point x + * @param {number} cpY - Control point y + * @param {number} cpX2 - Second Control point x + * @param {number} cpY2 - Second Control point y + * @param {number} toX - Destination point x + * @param {number} toY - Destination point y + * @return {number} Length of bezier curve + */ + _bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY) + { + const n = 10; + let result = 0.0; + let t = 0.0; + let t2 = 0.0; + let t3 = 0.0; + let nt = 0.0; + let nt2 = 0.0; + let nt3 = 0.0; + let x = 0.0; + let y = 0.0; + let dx = 0.0; + let dy = 0.0; + let prevX = fromX; + let prevY = fromY; + + for (let i = 1; i <= n; ++i) + { + t = i / n; + t2 = t * t; + t3 = t2 * t; + nt = (1.0 - t); + nt2 = nt * nt; + nt3 = nt2 * nt; + + x = (nt3 * fromX) + (3.0 * nt2 * t * cpX) + (3.0 * nt * t2 * cpX2) + (t3 * toX); + y = (nt3 * fromY) + (3.0 * nt2 * t * cpY) + (3 * nt * t2 * cpY2) + (t3 * toY); + dx = prevX - x; + dy = prevY - y; + prevX = x; + prevY = y; + + result += Math.sqrt((dx * dx) + (dy * dy)); + } + + return result; + } + + /** + * Calculate number of segments for the curve based on its length to ensure its smoothness. + * + * @private + * @param {number} length - length of curve + * @return {number} Number of segments + */ + _segmentsCount(length) + { + let result = Math.ceil(length / Graphics.CURVES.maxLength); + + if (result < Graphics.CURVES.minSegments) + { + result = Graphics.CURVES.minSegments; + } + else if (result > Graphics.CURVES.maxSegments) + { + result = Graphics.CURVES.maxSegments; + } + + return result; + } + + /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() * method or the drawCircle() method. * @@ -329,7 +448,6 @@ this.moveTo(0, 0); } - const n = 20; const points = this.currentPath.shape.points; let xa = 0; let ya = 0; @@ -341,6 +459,9 @@ const fromX = points[points.length - 2]; const fromY = points[points.length - 1]; + const n = Graphics.CURVES.adaptive + ? this._segmentsCount(this._quadraticCurveLength(fromX, fromY, cpX, cpY, toX, toY)) + : 20; for (let i = 1; i <= n; ++i) { @@ -390,7 +511,11 @@ points.length -= 2; - bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, points); + const n = Graphics.CURVES.adaptive + ? this._segmentsCount(this._bezierCurveLength(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY)) + : 20; + + bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, points); this.dirty++; @@ -496,7 +621,9 @@ } const sweep = endAngle - startAngle; - const segs = Math.ceil(Math.abs(sweep) / PI_2) * 40; + const segs = Graphics.CURVES.adaptive + ? this._segmentsCount(Math.abs(sweep) * radius) + : Math.ceil(Math.abs(sweep) / PI_2) * 40; if (sweep === 0) { @@ -1165,3 +1292,25 @@ } Graphics._SPRITE_TEXTURE = null; + +/** + * Graphics curves resolution settings. If `adaptive` flag is set to `true`, + * the resolution is calculated based on the curve's length to ensure better visual quality. + * Adaptive draw works with `bezierCurveTo` and `quadraticCurveTo`. + * + * @static + * @constant + * @memberof PIXI.Graphics + * @name CURVES + * @type {object} + * @property {boolean} adaptive=false - flag indicating if the resolution should be adaptive + * @property {number} maxLength=10 - maximal length of a single segment of the curve (if adaptive = false, ignored) + * @property {number} minSegments=8 - minimal number of segments in the curve (if adaptive = false, ignored) + * @property {number} maxSegments=2048 - maximal number of segments in the curve (if adaptive = false, ignored) + */ +Graphics.CURVES = { + adaptive: false, + maxLength: 10, + minSegments: 8, + maxSegments: 2048, +}; diff --git a/packages/graphics/src/utils/bezierCurveTo.js b/packages/graphics/src/utils/bezierCurveTo.js index fe73567..b02e68c 100644 --- a/packages/graphics/src/utils/bezierCurveTo.js +++ b/packages/graphics/src/utils/bezierCurveTo.js @@ -12,12 +12,12 @@ * @param {number} cpY2 - Second Control point y * @param {number} toX - Destination point x * @param {number} toY - Destination point y + * @param {number} n - Number of segments approximating the bezier curve * @param {number[]} [path=[]] - Path array to push points into * @return {number[]} Array of points of the curve */ -export default function bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, path = []) +export default function bezierCurveTo(fromX, fromY, cpX, cpY, cpX2, cpY2, toX, toY, n, path = []) { - const n = 20; let dt = 0; let dt2 = 0; let dt3 = 0;