1- import { almostEquals , sign } from './helpers.math' ;
1+ import { almostEquals , distanceBetweenPoints , sign } from './helpers.math' ;
22import { _isPointInArea } from './helpers.canvas' ;
33
44const EPSILON = Number . EPSILON || 1e-14 ;
5+ const getPoint = ( points , i ) => i < points . length && ! points [ i ] . skip && points [ i ] ;
56
67export function splineCurve ( firstPoint , middlePoint , afterPoint , t ) {
78 // Props to Rob Spencer at scaled innovation for his post on splining between points
@@ -12,9 +13,8 @@ export function splineCurve(firstPoint, middlePoint, afterPoint, t) {
1213 const previous = firstPoint . skip ? middlePoint : firstPoint ;
1314 const current = middlePoint ;
1415 const next = afterPoint . skip ? middlePoint : afterPoint ;
15-
16- const d01 = Math . sqrt ( Math . pow ( current . x - previous . x , 2 ) + Math . pow ( current . y - previous . y , 2 ) ) ;
17- const d12 = Math . sqrt ( Math . pow ( next . x - current . x , 2 ) + Math . pow ( next . y - current . y , 2 ) ) ;
16+ const d01 = distanceBetweenPoints ( current , previous ) ;
17+ const d12 = distanceBetweenPoints ( next , current ) ;
1818
1919 let s01 = d01 / ( d01 + d12 ) ;
2020 let s12 = d12 / ( d01 + d12 ) ;
@@ -38,114 +38,138 @@ export function splineCurve(firstPoint, middlePoint, afterPoint, t) {
3838 } ;
3939}
4040
41- export function splineCurveMonotone ( points ) {
42- // This function calculates Bézier control points in a similar way than |splineCurve|,
43- // but preserves monotonicity of the provided data and ensures no local extremums are added
44- // between the dataset discrete points due to the interpolation.
45- // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
46-
47- const pointsWithTangents = ( points || [ ] ) . map ( ( point ) => ( {
48- model : point ,
49- deltaK : 0 ,
50- mK : 0
51- } ) ) ;
52-
53- // Calculate slopes (deltaK) and initialize tangents (mK)
54- const pointsLen = pointsWithTangents . length ;
55- let i , pointBefore , pointCurrent , pointAfter ;
56- for ( i = 0 ; i < pointsLen ; ++ i ) {
57- pointCurrent = pointsWithTangents [ i ] ;
58- if ( pointCurrent . model . skip ) {
41+ /**
42+ * Adjust tangents to ensure monotonic properties
43+ */
44+ function monotoneAdjust ( points , deltaK , mK ) {
45+ const pointsLen = points . length ;
46+
47+ let alphaK , betaK , tauK , squaredMagnitude , pointCurrent ;
48+ let pointAfter = getPoint ( points , 0 ) ;
49+ for ( let i = 0 ; i < pointsLen - 1 ; ++ i ) {
50+ pointCurrent = pointAfter ;
51+ pointAfter = getPoint ( points , i + 1 ) ;
52+ if ( ! pointCurrent || ! pointAfter ) {
5953 continue ;
6054 }
6155
62- pointBefore = i > 0 ? pointsWithTangents [ i - 1 ] : null ;
63- pointAfter = i < pointsLen - 1 ? pointsWithTangents [ i + 1 ] : null ;
64- if ( pointAfter && ! pointAfter . model . skip ) {
65- const slopeDeltaX = ( pointAfter . model . x - pointCurrent . model . x ) ;
66-
67- // In the case of two points that appear at the same x pixel, slopeDeltaX is 0
68- pointCurrent . deltaK = slopeDeltaX !== 0 ? ( pointAfter . model . y - pointCurrent . model . y ) / slopeDeltaX : 0 ;
56+ if ( almostEquals ( deltaK [ i ] , 0 , EPSILON ) ) {
57+ mK [ i ] = mK [ i + 1 ] = 0 ;
58+ continue ;
6959 }
7060
71- if ( ! pointBefore || pointBefore . model . skip ) {
72- pointCurrent . mK = pointCurrent . deltaK ;
73- } else if ( ! pointAfter || pointAfter . model . skip ) {
74- pointCurrent . mK = pointBefore . deltaK ;
75- } else if ( sign ( pointBefore . deltaK ) !== sign ( pointCurrent . deltaK ) ) {
76- pointCurrent . mK = 0 ;
77- } else {
78- pointCurrent . mK = ( pointBefore . deltaK + pointCurrent . deltaK ) / 2 ;
61+ alphaK = mK [ i ] / deltaK [ i ] ;
62+ betaK = mK [ i + 1 ] / deltaK [ i ] ;
63+ squaredMagnitude = Math . pow ( alphaK , 2 ) + Math . pow ( betaK , 2 ) ;
64+ if ( squaredMagnitude <= 9 ) {
65+ continue ;
7966 }
67+
68+ tauK = 3 / Math . sqrt ( squaredMagnitude ) ;
69+ mK [ i ] = alphaK * tauK * deltaK [ i ] ;
70+ mK [ i + 1 ] = betaK * tauK * deltaK [ i ] ;
8071 }
72+ }
8173
82- // Adjust tangents to ensure monotonic properties
83- let alphaK , betaK , tauK , squaredMagnitude ;
84- for ( i = 0 ; i < pointsLen - 1 ; ++ i ) {
85- pointCurrent = pointsWithTangents [ i ] ;
86- pointAfter = pointsWithTangents [ i + 1 ] ;
87- if ( pointCurrent . model . skip || pointAfter . model . skip ) {
88- continue ;
89- }
74+ function monotoneCompute ( points , mK ) {
75+ const pointsLen = points . length ;
76+ let deltaX , pointBefore , pointCurrent ;
77+ let pointAfter = getPoint ( points , 0 ) ;
9078
91- if ( almostEquals ( pointCurrent . deltaK , 0 , EPSILON ) ) {
92- pointCurrent . mK = pointAfter . mK = 0 ;
79+ for ( let i = 0 ; i < pointsLen ; ++ i ) {
80+ pointBefore = pointCurrent ;
81+ pointCurrent = pointAfter ;
82+ pointAfter = getPoint ( points , i + 1 ) ;
83+ if ( ! pointCurrent ) {
9384 continue ;
9485 }
9586
96- alphaK = pointCurrent . mK / pointCurrent . deltaK ;
97- betaK = pointAfter . mK / pointCurrent . deltaK ;
98- squaredMagnitude = Math . pow ( alphaK , 2 ) + Math . pow ( betaK , 2 ) ;
99- if ( squaredMagnitude <= 9 ) {
100- continue ;
87+ const { x, y} = pointCurrent ;
88+ if ( pointBefore ) {
89+ deltaX = ( x - pointBefore . x ) / 3 ;
90+ pointCurrent . cp1x = x - deltaX ;
91+ pointCurrent . cp1y = y - deltaX * mK [ i ] ;
92+ }
93+ if ( pointAfter ) {
94+ deltaX = ( pointAfter . x - x ) / 3 ;
95+ pointCurrent . cp2x = x + deltaX ;
96+ pointCurrent . cp2y = y + deltaX * mK [ i ] ;
10197 }
102-
103- tauK = 3 / Math . sqrt ( squaredMagnitude ) ;
104- pointCurrent . mK = alphaK * tauK * pointCurrent . deltaK ;
105- pointAfter . mK = betaK * tauK * pointCurrent . deltaK ;
10698 }
99+ }
100+
101+ /**
102+ * This function calculates Bézier control points in a similar way than |splineCurve|,
103+ * but preserves monotonicity of the provided data and ensures no local extremums are added
104+ * between the dataset discrete points due to the interpolation.
105+ * See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
106+ *
107+ * @param {{
108+ * x: number,
109+ * y: number,
110+ * skip?: boolean,
111+ * cp1x?: number,
112+ * cp1y?: number,
113+ * cp2x?: number,
114+ * cp2y?: number,
115+ * }[] } points
116+ */
117+ export function splineCurveMonotone ( points ) {
118+ const pointsLen = points . length ;
119+ const deltaK = Array ( pointsLen ) . fill ( 0 ) ;
120+ const mK = Array ( pointsLen ) ;
121+
122+ // Calculate slopes (deltaK) and initialize tangents (mK)
123+ let i , pointBefore , pointCurrent ;
124+ let pointAfter = getPoint ( points , 0 ) ;
107125
108- // Compute control points
109- let deltaX ;
110126 for ( i = 0 ; i < pointsLen ; ++ i ) {
111- pointCurrent = pointsWithTangents [ i ] ;
112- if ( pointCurrent . model . skip ) {
127+ pointBefore = pointCurrent ;
128+ pointCurrent = pointAfter ;
129+ pointAfter = getPoint ( points , i + 1 ) ;
130+ if ( ! pointCurrent ) {
113131 continue ;
114132 }
115133
116- pointBefore = i > 0 ? pointsWithTangents [ i - 1 ] : null ;
117- pointAfter = i < pointsLen - 1 ? pointsWithTangents [ i + 1 ] : null ;
118- if ( pointBefore && ! pointBefore . model . skip ) {
119- deltaX = ( pointCurrent . model . x - pointBefore . model . x ) / 3 ;
120- pointCurrent . model . controlPointPreviousX = pointCurrent . model . x - deltaX ;
121- pointCurrent . model . controlPointPreviousY = pointCurrent . model . y - deltaX * pointCurrent . mK ;
122- }
123- if ( pointAfter && ! pointAfter . model . skip ) {
124- deltaX = ( pointAfter . model . x - pointCurrent . model . x ) / 3 ;
125- pointCurrent . model . controlPointNextX = pointCurrent . model . x + deltaX ;
126- pointCurrent . model . controlPointNextY = pointCurrent . model . y + deltaX * pointCurrent . mK ;
134+ if ( pointAfter ) {
135+ const slopeDeltaX = ( pointAfter . x - pointCurrent . x ) ;
136+
137+ // In the case of two points that appear at the same x pixel, slopeDeltaX is 0
138+ deltaK [ i ] = slopeDeltaX !== 0 ? ( pointAfter . y - pointCurrent . y ) / slopeDeltaX : 0 ;
127139 }
140+ mK [ i ] = ! pointBefore ? deltaK [ i ]
141+ : ! pointAfter ? deltaK [ i - 1 ]
142+ : ( sign ( deltaK [ i - 1 ] ) !== sign ( deltaK [ i ] ) ) ? 0
143+ : ( deltaK [ i - 1 ] + deltaK [ i ] ) / 2 ;
128144 }
145+
146+ monotoneAdjust ( points , deltaK , mK ) ;
147+
148+ monotoneCompute ( points , mK ) ;
129149}
130150
131151function capControlPoint ( pt , min , max ) {
132152 return Math . max ( Math . min ( pt , max ) , min ) ;
133153}
134154
135155function capBezierPoints ( points , area ) {
136- let i , ilen , point ;
156+ let i , ilen , point , inArea , inAreaPrev ;
157+ let inAreaNext = _isPointInArea ( points [ 0 ] , area ) ;
137158 for ( i = 0 , ilen = points . length ; i < ilen ; ++ i ) {
138- point = points [ i ] ;
139- if ( ! _isPointInArea ( point , area ) ) {
159+ inAreaPrev = inArea ;
160+ inArea = inAreaNext ;
161+ inAreaNext = i < ilen - 1 && _isPointInArea ( points [ i + 1 ] , area ) ;
162+ if ( ! inArea ) {
140163 continue ;
141164 }
142- if ( i > 0 && _isPointInArea ( points [ i - 1 ] , area ) ) {
143- point . controlPointPreviousX = capControlPoint ( point . controlPointPreviousX , area . left , area . right ) ;
144- point . controlPointPreviousY = capControlPoint ( point . controlPointPreviousY , area . top , area . bottom ) ;
165+ point = points [ i ] ;
166+ if ( inAreaPrev ) {
167+ point . cp1x = capControlPoint ( point . cp1x , area . left , area . right ) ;
168+ point . cp1y = capControlPoint ( point . cp1y , area . top , area . bottom ) ;
145169 }
146- if ( i < points . length - 1 && _isPointInArea ( points [ i + 1 ] , area ) ) {
147- point . controlPointNextX = capControlPoint ( point . controlPointNextX , area . left , area . right ) ;
148- point . controlPointNextY = capControlPoint ( point . controlPointNextY , area . top , area . bottom ) ;
170+ if ( inAreaNext ) {
171+ point . cp2x = capControlPoint ( point . cp2x , area . left , area . right ) ;
172+ point . cp2y = capControlPoint ( point . cp2y , area . top , area . bottom ) ;
149173 }
150174 }
151175}
@@ -173,10 +197,10 @@ export function _updateBezierControlPoints(points, options, area, loop) {
173197 points [ Math . min ( i + 1 , ilen - ( loop ? 0 : 1 ) ) % ilen ] ,
174198 options . tension
175199 ) ;
176- point . controlPointPreviousX = controlPoints . previous . x ;
177- point . controlPointPreviousY = controlPoints . previous . y ;
178- point . controlPointNextX = controlPoints . next . x ;
179- point . controlPointNextY = controlPoints . next . y ;
200+ point . cp1x = controlPoints . previous . x ;
201+ point . cp1y = controlPoints . previous . y ;
202+ point . cp2x = controlPoints . next . x ;
203+ point . cp2y = controlPoints . next . y ;
180204 prev = point ;
181205 }
182206 }
0 commit comments