1 /* string-lerp - progressively turn one string into another
2 Copyright 2014 Joe Wreschnig
3 Licensed under the terms of the GNU GPL v2 or later
4 @license http://www.gnu.org/licenses/gpl-2.0.html
5 @source: http://yukkurigames.com/string-lerp/
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License as
9 published by the Free Software Foundation; either version 2 of the
10 License, or (at your option) any later version.
12 This program is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 General Public License for more details.
17 As additional permission, you may distribute the program or works
18 based on it without the copy of the GNU GPL normally required,
19 provided you include this license notice and a URL through which
20 recipients can access the corresponding source code.
23 /*globals exports, Uint32Array */
28 var MAX_MATRIX_SIZE
= 256 * 256;
30 function costMatrix(source
, target
, ins
, del
, sub
) {
31 /** Calculate the Levenshtein cost matrix for source and target
33 If source and target are strings, they cannot contain any
34 astral or combining codepoints. Such data must be passed
35 as arrays of strings with one element per glyph.
37 ins, del, and sub are the costs for insertion, deletion,
38 and substition respectively. Their default value is 1. If
39 only ins is passed, del and sub are set to the same cost.
40 If ins and del are passed, sub is set to the more
43 The matrix is returned as a flat typed array.
45 Following http://en.wikipedia.org/wiki/Levenshtein_distance
47 ins
= ins
=== undefined ? 1 : (ins
| 0);
48 del
= (del
| 0) || ins
;
49 sub
= (sub
| 0) || Math
.max(ins
, del
);
50 var m
= source
.length
+ 1;
51 var n
= target
.length
+ 1;
52 var d
= new Uint32Array(m
* n
);
54 for (i
= 1; i
< m
; ++i
)
56 for (j
= 1; j
< n
; ++j
)
58 for (j
= 1; j
< n
; ++j
)
59 for (i
= 1; i
< m
; ++i
)
60 if (source
[i
- 1] === target
[j
- 1])
61 d
[n
* i
+ j
] = d
[n
* (i
- 1) + j
- 1];
63 d
[n
* i
+ j
] = Math
.min(del
+ d
[n
* (i
- 1) + j
],
64 ins
+ d
[n
* i
+ j
- 1],
65 sub
+ d
[n
* (i
- 1) + j
- 1]);
69 // First, note that deletion is just substition with nothing, so
70 // any DEL operation can be replaced by a SUB. Second, the
71 // operation code *is* the necessary slice offset for applying the
75 function editPath(costs
, target
) {
76 /** Given a cost matrix and a target, create an edit list */
78 var j
= target
.length
;
80 var i
= costs
.length
/ n
- 1;
82 var sub
= (i
&& j
) ? costs
[n
* (i
- 1) + j
- 1] : Infinity
;
83 var del
= i
? costs
[n
* (i
- 1) + j
] : Infinity
;
84 var ins
= j
? costs
[n
* i
+ j
- 1] : Infinity
;
85 if (sub
<= ins
&& sub
<= del
) {
86 if (costs
[n
* i
+ j
] !== costs
[n
* (i
- 1) + j
- 1])
87 path
.push([SUB
, i
- 1, target
[j
- 1]]);
89 } else if (ins
<= del
) {
90 path
.push([INS
, i
, target
[j
- 1]]);
93 path
.push([SUB
, i
- 1, ""]);
100 function diff(source
, target
, ins
, del
, sub
) {
101 /** Create a list of edits to turn source into target
103 ins, del, and sub are as passed to costMatrix.
105 return editPath(costMatrix(source
, target
, ins
, del
, sub
), target
);
108 function patchArray(diff
, source
) {
109 for (var i
= 0; i
< diff
.length
; ++i
) {
111 source
.splice(edit
[1], edit
[0], edit
[2]);
116 function patchString(diff
, source
) {
117 for (var i
= 0; i
< diff
.length
; ++i
) {
119 var head
= source
.slice(0, edit
[1]);
120 var tail
= source
.slice(edit
[1] + edit
[0]);
121 source
= head
+ edit
[2] + tail
;
126 function patch(diff
, source
) {
127 /** Apply a list of edits to source */
128 var patcher
= Array
.isArray(source
) ? patchArray
: patchString
;
129 return patcher(diff
, source
);
132 // Matches if a string contains combining characters or astral
133 // codepoints (technically, the first half surrogate of an astral
135 var MULTI
= /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uD800-\uDBFF\uFE20-\uFE2F]/;
137 // Match an entire (potentially astral) codepoint and any
138 // combining characters following it.
139 var GLYPH
= /[\0-\u02FF\u0370-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uD800-\uFE1F\uFE30-\uFFFF][\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uDC00-\uDFFF\uFE20-\uFE2F]*/g;
141 function diffLerpAstral(source
, target
, amount
) {
142 // This split is not perfect for all languages, but at least
143 // it won't create invalid surrogate pairs or orphaned
144 // combining characters.
145 var sourceGlyphs
= source
.match(GLYPH
) || [];
146 var targetGlyphs
= target
.match(GLYPH
) || [];
147 var edits
= diff(targetGlyphs
, sourceGlyphs
, 2, 2, 3);
148 // The edit path works from the string end, forwards, because
149 // that's how Levenshtein edits work. To match LTR reading
150 // direction (and the behavior of fastLerp), swap the strings
151 // and invert the parameter when editing.
152 var partial
= edits
.slice(0, Math
.round((1 - amount
) * edits
.length
));
153 return patchArray(partial
, targetGlyphs
).join("");
156 function diffLerpBasic(source
, target
, amount
) {
157 var edits
= diff(target
, source
, 2, 2, 3);
158 // The edit path works from the string end, forwards, because
159 // that's how Levenshtein edits work. To match LTR reading
160 // direction (and the behavior of fastLerp), swap the strings
161 // and invert the parameter when editing.
162 var partial
= edits
.slice(0, Math
.round((1 - amount
) * edits
.length
));
163 return patchString(partial
, target
);
166 function diffLerp(source
, target
, amount
) {
167 /** Interpolate between two strings using edit operations
169 This interpolation algorithm applys a partial edit of one
170 string into the other. This produces nice looking results,
171 but can take a significant amount of time and memory to
172 compute the edits. It is not recommended for strings
173 longer than a few hundred characters.
176 if (source
.match(MULTI
) || target
.match(MULTI
))
177 return diffLerpAstral(source
, target
, amount
);
179 return diffLerpBasic(source
, target
, amount
);
182 var NUMBERS
= /(-?\d{1,20}(?:\.\d{1,20})?)/g;
184 function areNumericTwins(source
, target
) {
185 /** Check if a and b differ only in numerals */
186 return source
.replace(NUMBERS
, "0") === target
.replace(NUMBERS
, "0");
189 function nlerp(source
, target
, amount
) {
190 return source
+ (target
- source
) * amount
;
193 function numericLerp(source
, target
, amount
) {
194 /** Interpolate numerically between strings containing numbers
196 Numbers may have a leading "-" and a single "." to mark
197 the decimal point, but something must be after the ".".
198 No other floating point syntax (e.g. 1e6) is supported.
199 They are treated as fixed-point values, with the point's
200 position itself interpolating.
202 For example, numericLerp("0.0", "100".0, 0.123) === "12.3"
203 because the "." in "0.0" is interpreted as a decimal
204 point. But numericLerp("0.", "100.", 0.123) === "12."
205 because the strings are interpreted as integers followed
208 Calling this functions on strings that differ in more than
209 numerals gives undefined results.
212 var targetParts
= target
.split(NUMBERS
);
215 while ((match
= NUMBERS
.exec(source
))) {
216 var sourcePart
= match
[0];
217 var targetPart
= targetParts
[i
];
218 var part
= nlerp(+sourcePart
, +targetPart
, amount
);
219 var sourcePoint
= sourcePart
.indexOf(".");
220 var targetPoint
= targetPart
.indexOf(".");
221 var point
= Math
.round(nlerp(
222 sourcePoint
>= 0 ? (sourcePart
.length
- 1) - sourcePoint
: 0,
223 targetPoint
>= 0 ? (targetPart
.length
- 1) - targetPoint
: 0,
225 targetParts
[i
] = part
.toFixed(point
);
228 return targetParts
.join("");
231 function fastLerpAstral(source
, target
, amount
) {
232 var sourceGlyphs
= source
.match(GLYPH
) || [];
233 var targetGlyphs
= target
.match(GLYPH
) || [];
234 var sourceLength
= Math
.round(sourceGlyphs
.length
* amount
);
235 var targetLength
= Math
.round(targetGlyphs
.length
* amount
);
236 var head
= targetGlyphs
.slice(0, targetLength
);
237 var tail
= sourceGlyphs
.slice(sourceLength
, sourceGlyphs
.length
);
238 head
.push
.apply(head
, tail
);
239 return head
.join("");
242 function fastLerpBasic(source
, target
, amount
) {
243 var sourceLength
= Math
.round(source
.length
* amount
);
244 var targetLength
= Math
.round(target
.length
* amount
);
245 var head
= target
.substring(0, targetLength
);
246 var tail
= source
.substring(sourceLength
, source
.length
);
250 function fastLerp(source
, target
, amount
) {
251 /** Interpolate between two strings based on length
253 This interpolation algorithm progressively replaces the
254 front of one string with another. This approach is fast
255 but does not look good when the strings are similar.
258 // TODO: Consider fast-pathing this even more for very large
259 // strings, e.g. in the megabyte range. These are large enough
260 // that it should be fine to just pick a codepoint and search
261 // for the nearest glyph start.
262 if (source
.match(MULTI
) || target
.match(MULTI
))
263 return fastLerpAstral(source
, target
, amount
);
265 return fastLerpBasic(source
, target
, amount
);
268 function lerp(source
, target
, amount
) {
269 /** Interpolate between two strings as best as possible
271 If the strings are identical aside from numbers in them,
272 they are passed through numericLerp.
274 If the strings are not numbers and short, they are passed
277 Otherwise, they are passed through fastLerp.
279 source
= source
.toString();
280 target
= target
.toString();
282 // Fast path for boundary cases.
283 if (amount
=== 0) return source
;
284 if (amount
=== 1) return target
;
286 if (areNumericTwins(source
, target
))
287 return numericLerp(source
, target
, amount
);
289 // Numeric lerps should over- and under-shoot when fed numbers
290 // outside 0 to 1, but other types cannot.
291 if (amount
< 0) return source
;
292 if (amount
> 1) return target
;
294 var n
= source
.length
* target
.length
;
295 var appropriate
= (n
&& n
< MAX_MATRIX_SIZE
) ? diffLerp
: fastLerp
;
296 return appropriate(source
, target
, amount
);
299 exports
.costMatrix
= costMatrix
;
300 exports
.patch
= patch
;
302 exports
.fastLerp
= fastLerp
;
303 exports
.diffLerp
= diffLerp
;
304 exports
.numericLerp
= numericLerp
;
307 })(typeof exports
=== "undefined" ? (this.stringLerp
= {}) : exports
);