bezierCurveToPolyline.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. const { sqrt, pow, ceil, abs } = Math
  2. // Initialize the number of points per curve
  3. const defaultSegmentPointsNum = 50
  4. /**
  5. * @example data structure of bezierCurve
  6. * bezierCurve = [
  7. * // Starting point of the curve
  8. * [10, 10],
  9. * // BezierCurve segment data (controlPoint1, controlPoint2, endPoint)
  10. * [
  11. * [20, 20], [40, 20], [50, 10]
  12. * ],
  13. * ...
  14. * ]
  15. */
  16. /**
  17. * @description Abstract the curve as a polyline consisting of N points
  18. * @param {Array} bezierCurve bezierCurve data
  19. * @param {Number} precision calculation accuracy. Recommended for 1-20. Default = 5
  20. * @return {Object} Calculation results and related data
  21. * @return {Array} Option.segmentPoints Point data that constitutes a polyline after calculation
  22. * @return {Number} Option.cycles Number of iterations
  23. * @return {Number} Option.rounds The number of recursions for the last iteration
  24. */
  25. function abstractBezierCurveToPolyline (bezierCurve, precision = 5) {
  26. const segmentsNum = bezierCurve.length - 1
  27. const startPoint = bezierCurve[0]
  28. const endPoint = bezierCurve[segmentsNum][2]
  29. const segments = bezierCurve.slice(1)
  30. const getSegmentTPointFuns = segments.map((seg, i) => {
  31. let beginPoint = (i === 0) ? startPoint : segments[i - 1][2]
  32. return createGetBezierCurveTPointFun(beginPoint, ...seg)
  33. })
  34. // Initialize the curve to a polyline
  35. let segmentPointsNum = new Array(segmentsNum).fill(defaultSegmentPointsNum)
  36. let segmentPoints = getSegmentPointsByNum(getSegmentTPointFuns, segmentPointsNum)
  37. // Calculate uniformly distributed points by iteratively
  38. const result = calcUniformPointsByIteration(segmentPoints, getSegmentTPointFuns, segments, precision)
  39. result.segmentPoints.push(endPoint)
  40. return result
  41. }
  42. /**
  43. * @description Generate a method for obtaining corresponding point by t according to curve data
  44. * @param {Array} beginPoint BezierCurve begin point. [x, y]
  45. * @param {Array} controlPoint1 BezierCurve controlPoint1. [x, y]
  46. * @param {Array} controlPoint2 BezierCurve controlPoint2. [x, y]
  47. * @param {Array} endPoint BezierCurve end point. [x, y]
  48. * @return {Function} Expected function
  49. */
  50. function createGetBezierCurveTPointFun (beginPoint, controlPoint1, controlPoint2, endPoint) {
  51. return function (t) {
  52. const tSubed1 = 1 - t
  53. const tSubed1Pow3 = pow(tSubed1, 3)
  54. const tSubed1Pow2 = pow(tSubed1, 2)
  55. const tPow3 = pow(t, 3)
  56. const tPow2 = pow(t, 2)
  57. return [
  58. beginPoint[0] * tSubed1Pow3 + 3 * controlPoint1[0] * t * tSubed1Pow2 + 3 * controlPoint2[0] * tPow2 * tSubed1 + endPoint[0] * tPow3,
  59. beginPoint[1] * tSubed1Pow3 + 3 * controlPoint1[1] * t * tSubed1Pow2 + 3 * controlPoint2[1] * tPow2 * tSubed1 + endPoint[1] * tPow3
  60. ]
  61. }
  62. }
  63. /**
  64. * @description Get the distance between two points
  65. * @param {Array} point1 BezierCurve begin point. [x, y]
  66. * @param {Array} point2 BezierCurve controlPoint1. [x, y]
  67. * @return {Number} Expected distance
  68. */
  69. function getTwoPointDistance ([ax, ay], [bx, by]) {
  70. return sqrt(pow(ax - bx, 2) + pow(ay - by, 2))
  71. }
  72. /**
  73. * @description Get the sum of the array of numbers
  74. * @param {Array} nums An array of numbers
  75. * @return {Number} Expected sum
  76. */
  77. function getNumsSum (nums) {
  78. return nums.reduce((sum, num) => sum + num, 0)
  79. }
  80. /**
  81. * @description Get the distance of multiple sets of points
  82. * @param {Array} segmentPoints Multiple sets of point data
  83. * @return {Array} Distance of multiple sets of point data
  84. */
  85. function getSegmentPointsDistance (segmentPoints) {
  86. return segmentPoints.map((points, i) => {
  87. return new Array(points.length - 1)
  88. .fill(0)
  89. .map((temp, j) => getTwoPointDistance(points[j], points[j + 1]))
  90. })
  91. }
  92. /**
  93. * @description Get the distance of multiple sets of points
  94. * @param {Array} segmentPoints Multiple sets of point data
  95. * @return {Array} Distance of multiple sets of point data
  96. */
  97. function getSegmentPointsByNum (getSegmentTPointFuns, segmentPointsNum) {
  98. return getSegmentTPointFuns.map((getSegmentTPointFun, i) => {
  99. const tGap = 1 / segmentPointsNum[i]
  100. return new Array(segmentPointsNum[i])
  101. .fill('')
  102. .map((foo, j) => getSegmentTPointFun(j * tGap))
  103. })
  104. }
  105. /**
  106. * @description Get the sum of deviations between line segment and the average length
  107. * @param {Array} segmentPointsDistance Segment length of polyline
  108. * @param {Number} avgLength Average length of the line segment
  109. * @return {Number} Deviations
  110. */
  111. function getAllDeviations (segmentPointsDistance, avgLength) {
  112. return segmentPointsDistance
  113. .map(seg => seg.map(s => abs(s - avgLength)))
  114. .map(seg => getNumsSum(seg))
  115. .reduce((total, v) => total + v, 0)
  116. }
  117. /**
  118. * @description Calculate uniformly distributed points by iteratively
  119. * @param {Array} segmentPoints Multiple setd of points that make up a polyline
  120. * @param {Array} getSegmentTPointFuns Functions of get a point on the curve with t
  121. * @param {Array} segments BezierCurve data
  122. * @param {Number} precision Calculation accuracy
  123. * @return {Object} Calculation results and related data
  124. * @return {Array} Option.segmentPoints Point data that constitutes a polyline after calculation
  125. * @return {Number} Option.cycles Number of iterations
  126. * @return {Number} Option.rounds The number of recursions for the last iteration
  127. */
  128. function calcUniformPointsByIteration (segmentPoints, getSegmentTPointFuns, segments, precision) {
  129. // The number of loops for the current iteration
  130. let rounds = 4
  131. // Number of iterations
  132. let cycles = 1
  133. do {
  134. // Recalculate the number of points per curve based on the last iteration data
  135. let totalPointsNum = segmentPoints.reduce((total, seg) => total + seg.length, 0)
  136. // Add last points of segment to calc exact segment length
  137. segmentPoints.forEach((seg, i) => seg.push(segments[i][2]))
  138. let segmentPointsDistance = getSegmentPointsDistance(segmentPoints)
  139. let lineSegmentNum = segmentPointsDistance.reduce((total, seg) => total + seg.length, 0)
  140. let segmentlength = segmentPointsDistance.map(seg => getNumsSum(seg))
  141. let totalLength = getNumsSum(segmentlength)
  142. let avgLength = totalLength / lineSegmentNum
  143. // Check if precision is reached
  144. let allDeviations = getAllDeviations(segmentPointsDistance, avgLength)
  145. if (allDeviations <= precision) break
  146. totalPointsNum = ceil(avgLength / precision * totalPointsNum * 1.1)
  147. const segmentPointsNum = segmentlength.map(length => ceil(length / totalLength * totalPointsNum))
  148. // Calculate the points after redistribution
  149. segmentPoints = getSegmentPointsByNum(getSegmentTPointFuns, segmentPointsNum)
  150. totalPointsNum = segmentPoints.reduce((total, seg) => total + seg.length, 0)
  151. let segmentPointsForLength = JSON.parse(JSON.stringify(segmentPoints))
  152. segmentPointsForLength.forEach((seg, i) => seg.push(segments[i][2]))
  153. segmentPointsDistance = getSegmentPointsDistance(segmentPointsForLength)
  154. lineSegmentNum = segmentPointsDistance.reduce((total, seg) => total + seg.length, 0)
  155. segmentlength = segmentPointsDistance.map(seg => getNumsSum(seg))
  156. totalLength = getNumsSum(segmentlength)
  157. avgLength = totalLength / lineSegmentNum
  158. const stepSize = 1 / totalPointsNum / 10
  159. // Recursively for each segment of the polyline
  160. getSegmentTPointFuns.forEach((getSegmentTPointFun, i) => {
  161. const currentSegmentPointsNum = segmentPointsNum[i]
  162. const t = new Array(currentSegmentPointsNum).fill('').map((foo, j) => j / segmentPointsNum[i])
  163. // Repeated recursive offset
  164. for (let r = 0; r < rounds; r++) {
  165. let distance = getSegmentPointsDistance([segmentPoints[i]])[0]
  166. const deviations = distance.map(d => d - avgLength)
  167. let offset = 0
  168. for (let j = 0; j < currentSegmentPointsNum; j++) {
  169. if (j === 0) return
  170. offset += deviations[j - 1]
  171. t[j] -= stepSize * offset
  172. if (t[j] > 1) t[j] = 1
  173. if (t[j] < 0) t[j] = 0
  174. segmentPoints[i][j] = getSegmentTPointFun(t[j])
  175. }
  176. }
  177. })
  178. rounds *= 4
  179. cycles++
  180. } while (rounds <= 1025)
  181. segmentPoints = segmentPoints.reduce((all, seg) => all.concat(seg), [])
  182. return {
  183. segmentPoints,
  184. cycles,
  185. rounds
  186. }
  187. }
  188. /**
  189. * @description Get the polyline corresponding to the Bezier curve
  190. * @param {Array} bezierCurve BezierCurve data
  191. * @param {Number} precision Calculation accuracy. Recommended for 1-20. Default = 5
  192. * @return {Array|Boolean} Point data that constitutes a polyline after calculation (Invalid input will return false)
  193. */
  194. export function bezierCurveToPolyline (bezierCurve, precision = 5) {
  195. if (!bezierCurve) {
  196. console.error('bezierCurveToPolyline: Missing parameters!')
  197. return false
  198. }
  199. if (!(bezierCurve instanceof Array)) {
  200. console.error('bezierCurveToPolyline: Parameter bezierCurve must be an array!')
  201. return false
  202. }
  203. if (typeof precision !== 'number') {
  204. console.error('bezierCurveToPolyline: Parameter precision must be a number!')
  205. return false
  206. }
  207. const { segmentPoints } = abstractBezierCurveToPolyline(bezierCurve, precision)
  208. return segmentPoints
  209. }
  210. /**
  211. * @description Get the bezier curve length
  212. * @param {Array} bezierCurve bezierCurve data
  213. * @param {Number} precision calculation accuracy. Recommended for 5-10. Default = 5
  214. * @return {Number|Boolean} BezierCurve length (Invalid input will return false)
  215. */
  216. export function getBezierCurveLength (bezierCurve, precision = 5) {
  217. if (!bezierCurve) {
  218. console.error('getBezierCurveLength: Missing parameters!')
  219. return false
  220. }
  221. if (!(bezierCurve instanceof Array)) {
  222. console.error('getBezierCurveLength: Parameter bezierCurve must be an array!')
  223. return false
  224. }
  225. if (typeof precision !== 'number') {
  226. console.error('getBezierCurveLength: Parameter precision must be a number!')
  227. return false
  228. }
  229. const { segmentPoints } = abstractBezierCurveToPolyline(bezierCurve, precision)
  230. // Calculate the total length of the points that make up the polyline
  231. const pointsDistance = getSegmentPointsDistance([segmentPoints])[0]
  232. const length = getNumsSum(pointsDistance)
  233. return length
  234. }
  235. export default bezierCurveToPolyline