blob: 89b8c297dc572ed1944d15f3cac4450760a8e5c9 [file] [log] [blame]
//
// Copyright 2014 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
//
part of charted.core.interpolators;
/// [Interpolator] accepts [t], such that 0.0 < t < 1.0 and returns
/// a value in a pre-defined range.
typedef Interpolator<S> = S Function(num t);
/// [InterpolatorGenerator] accepts two parameters [a], [b] and returns an
/// [Interpolator] for transitioning from [a] to [b]
typedef InterpolatorGenerator<T, S> = Interpolator<S> Function(T a, T b);
/// List of registered interpolators - [createInterpolatorFromRegistry]
/// iterates through this list from backwards and the first non-null
/// interpolate function is returned to the caller.
List<InterpolatorGenerator> _interpolators = [createInterpolatorByType];
/// Returns a default interpolator between values [a] and [b]. Unless
/// more interpolators are added, one of the internal implementations are
/// selected by the type of [a] and [b].
Interpolator createInterpolatorFromRegistry(a, b) {
Interpolator fn;
int i = _interpolators.length;
while (--i >= 0 && fn == null) {
fn = _interpolators[i](a, b);
}
return fn;
}
/// Creates an interpolator based on the type of [a] and [b].
///
/// Usage note: Use this method only when type of [a] and [b] are not known.
/// When used, this function will prevent tree shaking of all built-in
/// interpolators.
Interpolator createInterpolatorByType(a, b) {
if (a is List && b is List) {
return createListInterpolator(a, b);
} else if (a is Map && b is Map) {
return createMapInterpolator(a, b);
} else if (a is String && b is String) {
return createStringInterpolator(a, b);
} else if (a is num && b is num) {
return createNumberInterpolator(a, b);
} else if (a is Color && b is Color) {
return createRgbColorInterpolator(a, b);
} else {
return (t) => (t <= 0.5) ? a : b;
}
}
//
// Implementations of InterpolatorGenerator
//
/// Generate a numeric interpolator between numbers [a] and [b]
Interpolator<num> createNumberInterpolator(num a, num b) {
b -= a;
return (t) => a + b * t;
}
/// Generate a rounded number interpolator between numbers [a] and [b]
Interpolator<num> createRoundedNumberInterpolator(num a, num b) {
b -= a;
return (t) => (a + b * t).round();
}
/// Generate an interpolator between two strings [a] and [b].
///
/// The interpolator will interpolate all the number pairs in both strings
/// that have same number of numeric parts. The function assumes the non
/// number part of the string to be identical and would use string [b] for
/// merging the non numeric part of the strings.
///
/// Eg: Interpolate between $100.0 and $150.0
Interpolator<String> createStringInterpolator(String a, String b) {
if (a == null || b == null) return (num t) => b;
// See if both A and B represent colors as RGB or HEX strings.
// If yes, use color interpolators
if (Color.isRgbColorString(a) && Color.isRgbColorString(b)) {
return createRgbColorInterpolator(
new Color.fromRgbString(a), new Color.fromRgbString(b));
}
// See if both A and B represent colors as HSL strings.
// If yes, use color interpolators.
if (Color.isHslColorString(a) && Color.isHslColorString(b)) {
return createHslColorInterpolator(
new Color.fromHslString(a), new Color.fromHslString(b));
}
var numberRegEx = new RegExp(r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?');
var numMatchesInA = numberRegEx.allMatches(a);
var numMatchesInB = numberRegEx.allMatches(b);
List<String> stringParts = [];
List<String> numberPartsInA = [];
List<String> numberPartsInB = [];
List<Interpolator<num>> interpolators = [];
int s0 = 0;
numberPartsInA.addAll(numMatchesInA.map((m) => m.group(0)));
for (Match m in numMatchesInB) {
stringParts.add(b.substring(s0, m.start));
numberPartsInB.add(m.group(0));
s0 = m.end;
}
if (s0 < b.length) stringParts.add(b.substring(s0));
int numberLength = math.min(numberPartsInA.length, numberPartsInB.length);
int maxLength = math.max(numberPartsInA.length, numberPartsInB.length);
for (var i = 0; i < numberLength; i++) {
interpolators.add(createNumberInterpolator(
num.parse(numberPartsInA[i]), num.parse(numberPartsInB[i])));
}
if (numberPartsInA.length < numberPartsInB.length) {
for (var i = numberLength; i < maxLength; i++) {
interpolators.add(createNumberInterpolator(
num.parse(numberPartsInB[i]), num.parse(numberPartsInB[i])));
}
}
return (num t) {
StringBuffer sb = new StringBuffer();
for (var i = 0; i < stringParts.length; i++) {
sb.write(stringParts[i]);
if (interpolators.length > i) {
sb.write(interpolators[i](t));
}
}
return sb.toString();
};
}
/// Generate an interpolator for RGB values.
Interpolator<String> createRgbColorInterpolator(Color a, Color b) {
if (a == null || b == null) return (num t) => b.toRgbaString();
var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;
return (num t) => new Color.fromRgba((ar + br * t).round(),
(ag + bg * t).round(), (ab + bb * t).round(), 1.0)
.toRgbaString();
}
/// Generate an interpolator using HSL color system converted to Hex string.
Interpolator<String> createHslColorInterpolator(Color a, Color b) {
if (a == null || b == null) return (num t) => b.toHslaString();
var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al;
return (num t) => new Color.fromHsla((ah + bh * t).round(),
(as + bs * t).round(), (al + bl * t).round(), 1.0)
.toHslaString();
}
/// Generates an interpolator to interpolate each element between lists
/// [a] and [b] using registered interpolators.
Interpolator<List> createListInterpolator(List a, List b) {
if (a == null || b == null) return (t) => b;
var x = <Interpolator>[],
aLength = a.length,
numInterpolated = b.length,
output =
new List<dynamic>.filled(math.max(aLength, numInterpolated), null);
num n0 = math.min(aLength, numInterpolated);
int i;
for (i = 0; i < n0; i++) x.add(createInterpolatorFromRegistry(a[i], b[i]));
for (; i < aLength; ++i) output[i] = a[i];
for (; i < numInterpolated; ++i) output[i] = b[i];
return (num t) {
for (i = 0; i < n0; ++i) output[i] = x[i](t);
return output;
};
}
/// Generates an interpolator to interpolate each value on [a] to [b] using
/// registered interpolators.
Interpolator<Map> createMapInterpolator(Map a, Map b) {
if (a == null || b == null) return (t) => b;
var interpolatorsMap = new Map(),
output = new Map(),
aKeys = a.keys.toList(),
bKeys = b.keys.toList();
aKeys.forEach((k) {
if (b[k] != null) {
interpolatorsMap[k] = (createInterpolatorFromRegistry(a[k], b[k]));
} else {
output[k] = a[k];
}
});
bKeys.forEach((k) {
if (output[k] == null) {
output[k] = b[k];
}
});
return (t) {
interpolatorsMap.forEach((k, v) => output[k] = v(t));
return output;
};
}
/// Returns the interpolator that interpolators two transform strings
/// [a] and [b] by their translate, rotate, scale and skewX parts.
Interpolator<String> createTransformInterpolator(String a, String b) {
if (a == null || b == null) return (t) => b;
var numRegExStr = r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?',
numberRegEx = new RegExp(numRegExStr),
translateRegEx =
new RegExp(r'translate\(' + '$numRegExStr,$numRegExStr' + r'\)'),
scaleRegEx =
new RegExp(r'scale\(' + numRegExStr + r',' + numRegExStr + r'\)'),
rotateRegEx = new RegExp(r'rotate\(' + numRegExStr + r'(deg)?\)'),
skewRegEx = new RegExp(r'skewX\(' + numRegExStr + r'(deg)?\)'),
translateA = translateRegEx.firstMatch(a),
scaleA = scaleRegEx.firstMatch(a),
rotateA = rotateRegEx.firstMatch(a),
skewA = skewRegEx.firstMatch(a),
translateB = translateRegEx.firstMatch(b),
scaleB = scaleRegEx.firstMatch(b),
rotateB = rotateRegEx.firstMatch(b),
skewB = skewRegEx.firstMatch(b);
List<num> numSetA = [], numSetB = [];
String tempStr;
// translate
if (translateA != null) {
tempStr = a.substring(translateA.start, translateA.end);
var match = numberRegEx.allMatches(tempStr);
for (Match m in match) {
numSetA.add(num.parse(m.group(0)));
}
} else {
numSetA.addAll(const [0, 0]);
}
if (translateB != null) {
tempStr = b.substring(translateB.start, translateB.end);
var match = numberRegEx.allMatches(tempStr);
for (Match m in match) {
numSetB.add(num.parse(m.group(0)));
}
} else {
numSetB.addAll(const [0, 0]);
}
// scale
if (scaleA != null) {
tempStr = a.substring(scaleA.start, scaleA.end);
var match = numberRegEx.allMatches(tempStr);
for (Match m in match) {
numSetA.add(num.parse(m.group(0)));
}
} else {
numSetA.addAll(const [1, 1]);
}
if (scaleB != null) {
tempStr = b.substring(scaleB.start, scaleB.end);
var match = numberRegEx.allMatches(tempStr);
for (Match m in match) {
numSetB.add(num.parse(m.group(0)));
}
} else {
numSetB.addAll(const [1, 1]);
}
// rotate
if (rotateA != null) {
tempStr = a.substring(rotateA.start, rotateA.end);
var match = numberRegEx.firstMatch(tempStr);
numSetA.add(num.parse(match.group(0)));
} else {
numSetA.add(0);
}
if (rotateB != null) {
tempStr = b.substring(rotateB.start, rotateB.end);
var match = numberRegEx.firstMatch(tempStr);
numSetB.add(num.parse(match.group(0)));
} else {
numSetB.add(0);
}
// rotate < 180 degree
if (numSetA[4] != numSetB[4]) {
if (numSetA[4] - numSetB[4] > 180) {
numSetB[4] += 360;
} else if (numSetB[4] - numSetA[4] > 180) {
numSetA[4] += 360;
}
}
// skew
if (skewA != null) {
tempStr = a.substring(skewA.start, skewA.end);
var match = numberRegEx.firstMatch(tempStr);
numSetA.add(num.parse(match.group(0)));
} else {
numSetA.add(0);
}
if (skewB != null) {
tempStr = b.substring(skewB.start, skewB.end);
var match = numberRegEx.firstMatch(tempStr);
numSetB.add(num.parse(match.group(0)));
} else {
numSetB.add(0);
}
return (num t) {
return 'translate(${createNumberInterpolator(numSetA[0], numSetB[0])(t)},'
'${createNumberInterpolator(numSetA[1], numSetB[1])(t)})'
'scale(${createNumberInterpolator(numSetA[2], numSetB[2])(t)},'
'${createNumberInterpolator(numSetA[3], numSetB[3])(t)})'
'rotate(${createNumberInterpolator(numSetA[4], numSetB[4])(t)})'
'skewX(${createNumberInterpolator(numSetA[5], numSetB[5])(t)})';
};
}
/// Returns the interpolator that interpolators zoom list [a] to [b]. Zoom
/// lists are described by triple elements [ux0, uy0, w0] and [ux1, uy1, w1].
Interpolator<List<num>> createZoomInterpolator(List a, List b) {
if (a == null || b == null) return (t) => b;
assert(a.length == b.length && a.length == 3);
var sqrt2 = math.SQRT2, param2 = 2, param4 = 4;
num ux0 = a[0], uy0 = a[1], w0 = a[2], ux1 = b[0], uy1 = b[1], w1 = b[2];
num dx = ux1 - ux0,
dy = uy1 - uy0,
d2 = dx * dx + dy * dy,
d1 = math.sqrt(d2),
b0 = (w1 * w1 - w0 * w0 + param4 * d2) / (2 * w0 * param2 * d1),
b1 = (w1 * w1 - w0 * w0 - param4 * d2) / (2 * w1 * param2 * d1),
r0 = math.log(math.sqrt(b0 * b0 + 1) - b0),
r1 = math.log(math.sqrt(b1 * b1 + 1) - b1),
dr = r1 - r0,
S = ((!dr.isNaN) ? dr : math.log(w1 / w0)) / sqrt2;
return (num t) {
var s = t * S;
if (!dr.isNaN) {
// General case.
var coshr0 = cosh(r0),
u = w0 / (param2 * d1) * (coshr0 * tanh(sqrt2 * s + r0) - sinh(r0));
return [ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / cosh(sqrt2 * s + r0)];
}
// Special case for u0 ~= u1.
return <num>[ux0 + t * dx, uy0 + t * dy, w0 * math.exp(sqrt2 * s)];
};
}
/// Reverse interpolator for a number.
Interpolator<num> uninterpolateNumber(num a, num b) {
b = 1 / (b - a);
return (x) => (x - a) * b;
}
/// Reverse interpolator for a clamped number.
Interpolator<num> uninterpolateClamp(num a, num b) {
b = 1 / (b - a);
return (x) => math.max(0, math.min(1, (x - a) * b));
}