OLD | NEW |
| (Empty) |
1 // | |
2 // Copyright 2014 Google Inc. All rights reserved. | |
3 // | |
4 // Use of this source code is governed by a BSD-style | |
5 // license that can be found in the LICENSE file or at | |
6 // https://developers.google.com/open-source/licenses/bsd | |
7 // | |
8 | |
9 part of charted.core.interpolators; | |
10 | |
11 /// [Interpolator] accepts [t], such that 0.0 < t < 1.0 and returns | |
12 /// a value in a pre-defined range. | |
13 typedef Interpolator(num t); | |
14 | |
15 /// [InterpolatorGenerator] accepts two parameters [a], [b] and returns an | |
16 /// [Interpolator] for transitioning from [a] to [b] | |
17 typedef Interpolator InterpolatorGenerator(a, b); | |
18 | |
19 /// List of registered interpolators - [createInterpolatorFromRegistry] | |
20 /// iterates through this list from backwards and the first non-null | |
21 /// interpolate function is returned to the caller. | |
22 List<InterpolatorGenerator> _interpolators = [ createInterpolatorByType ]; | |
23 | |
24 /// Returns a default interpolator between values [a] and [b]. Unless | |
25 /// more interpolators are added, one of the internal implementations are | |
26 /// selected by the type of [a] and [b]. | |
27 Interpolator createInterpolatorFromRegistry(a, b) { | |
28 var fn, i = _interpolators.length; | |
29 while (--i >= 0 && fn == null) { | |
30 fn = _interpolators[i](a, b); | |
31 } | |
32 return fn; | |
33 } | |
34 | |
35 /// Creates an interpolator based on the type of [a] and [b]. | |
36 /// | |
37 /// Usage note: Use this method only when type of [a] and [b] are not known. | |
38 /// When used, this function will prevent tree shaking of all built-in | |
39 /// interpolators. | |
40 Interpolator createInterpolatorByType(a, b) => | |
41 (a is List && b is List) ? createListInterpolator(a, b) : | |
42 (a is Map && b is Map) ? createMapInterpolator(a, b) : | |
43 (a is String && b is String) ? createStringInterpolator(a, b) : | |
44 (a is num && b is num) ? createNumberInterpolator(a, b) : | |
45 (a is Color && b is Color) ? createRgbColorInterpolator(a, b) : | |
46 (t) => (t <= 0.5) ? a : b; | |
47 | |
48 | |
49 // | |
50 // Implementations of InterpolatorGenerator | |
51 // | |
52 | |
53 /// Generate a numeric interpolator between numbers [a] and [b] | |
54 Interpolator createNumberInterpolator(num a, num b) { | |
55 b -= a; | |
56 return (t) => a + b * t; | |
57 } | |
58 | |
59 /// Generate a rounded number interpolator between numbers [a] and [b] | |
60 Interpolator createRoundedNumberInterpolator(num a, num b) { | |
61 b -= a; | |
62 return (t) => (a + b * t).round(); | |
63 } | |
64 | |
65 | |
66 /// Generate an interpolator between two strings [a] and [b]. | |
67 /// | |
68 /// The interpolator will interpolate all the number pairs in both strings | |
69 /// that have same number of numeric parts. The function assumes the non | |
70 /// number part of the string to be identical and would use string [b] for | |
71 /// merging the non numeric part of the strings. | |
72 /// | |
73 /// Eg: Interpolate between $100.0 and $150.0 | |
74 Interpolator createStringInterpolator(String a, String b) { | |
75 if (a == null || b == null) return (t) => b; | |
76 | |
77 // See if both A and B represent colors as RGB or HEX strings. | |
78 // If yes, use color interpolators | |
79 if (Color.isRgbColorString(a) && Color.isRgbColorString(b)) { | |
80 return createRgbColorInterpolator( | |
81 new Color.fromRgbString(a), new Color.fromRgbString(b)); | |
82 } | |
83 | |
84 // See if both A and B represent colors as HSL strings. | |
85 // If yes, use color interpolators. | |
86 if (Color.isHslColorString(a) && Color.isHslColorString(b)) { | |
87 return createHslColorInterpolator( | |
88 new Color.fromHslString(a), new Color.fromHslString(b)); | |
89 } | |
90 | |
91 var numberRegEx = | |
92 new RegExp(r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?'), | |
93 numMatchesInA = numberRegEx.allMatches(a), | |
94 numMatchesInB = numberRegEx.allMatches(b), | |
95 stringParts = [], | |
96 numberPartsInA = [], | |
97 numberPartsInB = [], | |
98 interpolators = [], | |
99 s0 = 0; | |
100 | |
101 numberPartsInA.addAll(numMatchesInA.map((m) => m.group(0))); | |
102 | |
103 for (Match m in numMatchesInB) { | |
104 stringParts.add(b.substring(s0, m.start)); | |
105 numberPartsInB.add(m.group(0)); | |
106 s0 = m.end; | |
107 } | |
108 | |
109 if (s0 < b.length) stringParts.add(b.substring(s0)); | |
110 | |
111 int numberLength = math.min(numberPartsInA.length, numberPartsInB.length); | |
112 int maxLength = math.max(numberPartsInA.length, numberPartsInB.length); | |
113 for (var i = 0; i < numberLength; i++) { | |
114 interpolators.add(createNumberInterpolator(num.parse(numberPartsInA[i]), | |
115 num.parse(numberPartsInB[i]))); | |
116 } | |
117 if (numberPartsInA.length < numberPartsInB.length) { | |
118 for (var i = numberLength; i < maxLength; i++) { | |
119 interpolators.add(createNumberInterpolator(num.parse(numberPartsInB[i]), | |
120 num.parse(numberPartsInB[i]))); | |
121 } | |
122 } | |
123 | |
124 return (t) { | |
125 StringBuffer sb = new StringBuffer(); | |
126 for (var i = 0; i < stringParts.length; i++) { | |
127 sb.write(stringParts[i]); | |
128 if (interpolators.length > i) { | |
129 sb.write(interpolators[i](t)); | |
130 } | |
131 } | |
132 return sb.toString(); | |
133 }; | |
134 } | |
135 | |
136 /// Generate an interpolator for RGB values. | |
137 Interpolator createRgbColorInterpolator(Color a, Color b) { | |
138 if (a == null || b == null) return (t) => b; | |
139 var ar = a.r, | |
140 ag = a.g, | |
141 ab = a.b, | |
142 br = b.r - ar, | |
143 bg = b.g - ag, | |
144 bb = b.b - ab; | |
145 | |
146 return (t) => new Color.fromRgba((ar + br * t).round(), | |
147 (ag + bg * t).round(), (ab + bb * t).round(), 1.0).toRgbaString(); | |
148 } | |
149 | |
150 /// Generate an interpolator using HSL color system converted to Hex string. | |
151 Interpolator createHslColorInterpolator(Color a, Color b) { | |
152 if (a == null || b == null) return (t) => b; | |
153 var ah = a.h, | |
154 as = a.s, | |
155 al = a.l, | |
156 bh = b.h - ah, | |
157 bs = b.s - as, | |
158 bl = b.l - al; | |
159 | |
160 return (t) => new Color.fromHsla((ah + bh * t).round(), | |
161 (as + bs * t).round(), (al + bl * t).round(), 1.0).toHslaString(); | |
162 } | |
163 | |
164 /// Generates an interpolator to interpolate each element between lists | |
165 /// [a] and [b] using registered interpolators. | |
166 Interpolator createListInterpolator(List a, List b) { | |
167 if (a == null || b == null) return (t) => b; | |
168 var x = [], | |
169 aLength = a.length, | |
170 numInterpolated = b.length, | |
171 n0 = math.min(aLength, numInterpolated), | |
172 output = new List.filled(math.max(aLength, numInterpolated), null), | |
173 i; | |
174 | |
175 for (i = 0; i < n0; i++) x.add(createInterpolatorFromRegistry(a[i], b[i])); | |
176 for (; i < aLength; ++i) output[i] = a[i]; | |
177 for (; i < numInterpolated; ++i) output[i] = b[i]; | |
178 | |
179 return (t) { | |
180 for (i = 0; i < n0; ++i) output[i] = x[i](t); | |
181 return output; | |
182 }; | |
183 } | |
184 | |
185 /// Generates an interpolator to interpolate each value on [a] to [b] using | |
186 /// registered interpolators. | |
187 Interpolator createMapInterpolator(Map a, Map b) { | |
188 if (a == null || b == null) return (t) => b; | |
189 var interpolatorsMap = new Map(), | |
190 output = new Map(), | |
191 aKeys = a.keys.toList(), | |
192 bKeys = b.keys.toList(); | |
193 | |
194 aKeys.forEach((k) { | |
195 if (b[k] != null) { | |
196 interpolatorsMap[k] = (createInterpolatorFromRegistry(a[k], b[k])); | |
197 } else { | |
198 output[k] = a[k]; | |
199 } | |
200 }); | |
201 | |
202 bKeys.forEach((k) { | |
203 if (output[k] == null) { | |
204 output[k] = b[k]; | |
205 } | |
206 }); | |
207 | |
208 return (t) { | |
209 interpolatorsMap.forEach((k, v) => output[k] = v(t)); | |
210 return output; | |
211 }; | |
212 } | |
213 | |
214 /// Returns the interpolator that interpolators two transform strings | |
215 /// [a] and [b] by their translate, rotate, scale and skewX parts. | |
216 Interpolator createTransformInterpolator(String a, String b) { | |
217 if (a == null || b == null) return (t) => b; | |
218 var numRegExStr = r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?', | |
219 numberRegEx = new RegExp(numRegExStr), | |
220 translateRegEx = | |
221 new RegExp(r'translate\(' + '$numRegExStr,$numRegExStr' + r'\)'), | |
222 scaleRegEx = | |
223 new RegExp(r'scale\(' + numRegExStr + r',' + numRegExStr + r'\)'), | |
224 rotateRegEx = new RegExp(r'rotate\(' + numRegExStr + r'(deg)?\)'), | |
225 skewRegEx = new RegExp(r'skewX\(' + numRegExStr + r'(deg)?\)'), | |
226 | |
227 translateA = translateRegEx.firstMatch(a), | |
228 scaleA = scaleRegEx.firstMatch(a), | |
229 rotateA = rotateRegEx.firstMatch(a), | |
230 skewA = skewRegEx.firstMatch(a), | |
231 | |
232 translateB = translateRegEx.firstMatch(b), | |
233 scaleB = scaleRegEx.firstMatch(b), | |
234 rotateB = rotateRegEx.firstMatch(b), | |
235 skewB = skewRegEx.firstMatch(b); | |
236 | |
237 var numSetA = [], | |
238 numSetB = [], | |
239 tempStr, match; | |
240 | |
241 // translate | |
242 if (translateA != null) { | |
243 tempStr = a.substring(translateA.start, translateA.end); | |
244 match = numberRegEx.allMatches(tempStr); | |
245 for (Match m in match) { | |
246 numSetA.add(num.parse(m.group(0))); | |
247 } | |
248 } else { | |
249 numSetA.addAll(const[0, 0]); | |
250 } | |
251 | |
252 if (translateB != null) { | |
253 tempStr = b.substring(translateB.start, translateB.end); | |
254 match = numberRegEx.allMatches(tempStr); | |
255 for (Match m in match) { | |
256 numSetB.add(num.parse(m.group(0))); | |
257 } | |
258 } else { | |
259 numSetB.addAll(const[0, 0]); | |
260 } | |
261 | |
262 // scale | |
263 if (scaleA != null) { | |
264 tempStr = a.substring(scaleA.start, scaleA.end); | |
265 match = numberRegEx.allMatches(tempStr); | |
266 for (Match m in match) { | |
267 numSetA.add(num.parse(m.group(0))); | |
268 } | |
269 } else { | |
270 numSetA.addAll(const[1, 1]); | |
271 } | |
272 | |
273 if (scaleB != null) { | |
274 tempStr = b.substring(scaleB.start, scaleB.end); | |
275 match = numberRegEx.allMatches(tempStr); | |
276 for (Match m in match) { | |
277 numSetB.add(num.parse(m.group(0))); | |
278 } | |
279 } else { | |
280 numSetB.addAll(const[1, 1]); | |
281 } | |
282 | |
283 // rotate | |
284 if (rotateA != null) { | |
285 tempStr = a.substring(rotateA.start, rotateA.end); | |
286 match = numberRegEx.firstMatch(tempStr); | |
287 numSetA.add(num.parse(match.group(0))); | |
288 } else { | |
289 numSetA.add(0); | |
290 } | |
291 | |
292 if (rotateB != null) { | |
293 tempStr = b.substring(rotateB.start, rotateB.end); | |
294 match = numberRegEx.firstMatch(tempStr); | |
295 numSetB.add(num.parse(match.group(0))); | |
296 } else { | |
297 numSetB.add(0); | |
298 } | |
299 | |
300 // rotate < 180 degree | |
301 if (numSetA[4] != numSetB[4]) { | |
302 if (numSetA[4] - numSetB[4] > 180) { | |
303 numSetB[4] += 360; | |
304 } else if (numSetB[4] - numSetA[4] > 180) { | |
305 numSetA[4] += 360; | |
306 } | |
307 } | |
308 | |
309 // skew | |
310 if (skewA != null) { | |
311 tempStr = a.substring(skewA.start, skewA.end); | |
312 match = numberRegEx.firstMatch(tempStr); | |
313 numSetA.add(num.parse(match.group(0))); | |
314 } else { | |
315 numSetA.add(0); | |
316 } | |
317 | |
318 if (skewB != null) { | |
319 tempStr = b.substring(skewB.start, skewB.end); | |
320 match = numberRegEx.firstMatch(tempStr); | |
321 numSetB.add(num.parse(match.group(0))); | |
322 } else { | |
323 numSetB.add(0); | |
324 } | |
325 | |
326 return (t) { | |
327 return | |
328 'translate(${createNumberInterpolator(numSetA[0], numSetB[0])(t)},' | |
329 '${createNumberInterpolator(numSetA[1], numSetB[1])(t)})' | |
330 'scale(${createNumberInterpolator(numSetA[2], numSetB[2])(t)},' | |
331 '${createNumberInterpolator(numSetA[3], numSetB[3])(t)})' | |
332 'rotate(${createNumberInterpolator(numSetA[4], numSetB[4])(t)})' | |
333 'skewX(${createNumberInterpolator(numSetA[5], numSetB[5])(t)})'; | |
334 }; | |
335 } | |
336 | |
337 /// Returns the interpolator that interpolators zoom list [a] to [b]. Zoom | |
338 /// lists are described by triple elements [ux0, uy0, w0] and [ux1, uy1, w1]. | |
339 Interpolator createZoomInterpolator(List a, List b) { | |
340 if (a == null || b == null) return (t) => b; | |
341 assert(a.length == b.length && a.length == 3); | |
342 | |
343 var sqrt2 = math.SQRT2, | |
344 param2 = 2, | |
345 param4 = 4; | |
346 | |
347 var ux0 = a[0], uy0 = a[1], w0 = a[2], | |
348 ux1 = b[0], uy1 = b[1], w1 = b[2]; | |
349 | |
350 var dx = ux1 - ux0, | |
351 dy = uy1 - uy0, | |
352 d2 = dx * dx + dy * dy, | |
353 d1 = math.sqrt(d2), | |
354 b0 = (w1 * w1 - w0 * w0 + param4 * d2) / (2 * w0 * param2 * d1), | |
355 b1 = (w1 * w1 - w0 * w0 - param4 * d2) / (2 * w1 * param2 * d1), | |
356 r0 = math.log(math.sqrt(b0 * b0 + 1) - b0), | |
357 r1 = math.log(math.sqrt(b1 * b1 + 1) - b1), | |
358 dr = r1 - r0, | |
359 S = ((!dr.isNaN) ? dr : math.log(w1 / w0)) / sqrt2; | |
360 | |
361 return (t) { | |
362 var s = t * S; | |
363 if (!dr.isNaN) { | |
364 // General case. | |
365 var coshr0 = cosh(r0), | |
366 u = w0 / (param2 * d1) * (coshr0 * tanh(sqrt2 * s + r0) - sinh(r0)); | |
367 return [ | |
368 ux0 + u * dx, | |
369 uy0 + u * dy, | |
370 w0 * coshr0 / cosh(sqrt2 * s + r0) | |
371 ]; | |
372 } | |
373 // Special case for u0 ~= u1. | |
374 return [ | |
375 ux0 + t * dx, | |
376 uy0 + t * dy, | |
377 w0 * math.exp(sqrt2 * s) | |
378 ]; | |
379 }; | |
380 } | |
381 | |
382 /// Reverse interpolator for a number. | |
383 Interpolator uninterpolateNumber(num a, num b) { | |
384 b = 1 / (b - a); | |
385 return (x) => (x - a) * b; | |
386 } | |
387 | |
388 /// Reverse interpolator for a clamped number. | |
389 Interpolator uninterpolateClamp(num a, num b) { | |
390 b = 1 / (b - a); | |
391 return (x) => math.max(0, math.min(1, (x - a) * b)); | |
392 } | |
OLD | NEW |