Disable DPR > 1 on Safari due to bugs and performance issues.
[pwl6.git] / src / ext / string-lerp.js
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/
6 *//*
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.
11
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.
16
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.
21 */
22
23 /*globals exports, Uint32Array */
24
25 (function (exports) {
26 "use strict";
27
28 var MAX_MATRIX_SIZE = 256 * 256;
29
30 function costMatrix(source, target, ins, del, sub) {
31 /** Calculate the Levenshtein cost matrix for source and target
32
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.
36
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
41 expensive of the two.
42
43 The matrix is returned as a flat typed array.
44
45 Following http://en.wikipedia.org/wiki/Levenshtein_distance
46 */
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);
53 var i, j;
54 for (i = 1; i < m; ++i)
55 d[n * i] = i;
56 for (j = 1; j < n; ++j)
57 d[j] = 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];
62 else
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]);
66 return d;
67 }
68
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
72 // diff.
73 var INS = 0, SUB = 1;
74
75 function editPath(costs, target) {
76 /** Given a cost matrix and a target, create an edit list */
77 var path = [];
78 var j = target.length;
79 var n = j + 1;
80 var i = costs.length / n - 1;
81 while (i || j) {
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]]);
88 --i; --j;
89 } else if (ins <= del) {
90 path.push([INS, i, target[j - 1]]);
91 --j;
92 } else {
93 path.push([SUB, i - 1, ""]);
94 --i;
95 }
96 }
97 return path;
98 }
99
100 function diff(source, target, ins, del, sub) {
101 /** Create a list of edits to turn source into target
102
103 ins, del, and sub are as passed to costMatrix.
104 */
105 return editPath(costMatrix(source, target, ins, del, sub), target);
106 }
107
108 function patchArray(diff, source) {
109 for (var i = 0; i < diff.length; ++i) {
110 var edit = diff[i];
111 source.splice(edit[1], edit[0], edit[2]);
112 }
113 return source;
114 }
115
116 function patchString(diff, source) {
117 for (var i = 0; i < diff.length; ++i) {
118 var edit = diff[i];
119 var head = source.slice(0, edit[1]);
120 var tail = source.slice(edit[1] + edit[0]);
121 source = head + edit[2] + tail;
122 }
123 return source;
124 }
125
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);
130 }
131
132 // Matches if a string contains combining characters or astral
133 // codepoints (technically, the first half surrogate of an astral
134 // codepoint).
135 var MULTI = /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uD800-\uDBFF\uFE20-\uFE2F]/;
136
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;
140
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("");
154 }
155
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);
164 }
165
166 function diffLerp(source, target, amount) {
167 /** Interpolate between two strings using edit operations
168
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.
174 */
175
176 if (source.match(MULTI) || target.match(MULTI))
177 return diffLerpAstral(source, target, amount);
178 else
179 return diffLerpBasic(source, target, amount);
180 }
181
182 var NUMBERS = /(-?\d{1,20}(?:\.\d{1,20})?)/g;
183
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");
187 }
188
189 function nlerp(source, target, amount) {
190 return source + (target - source) * amount;
191 }
192
193 function numericLerp(source, target, amount) {
194 /** Interpolate numerically between strings containing numbers
195
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.
201
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
206 by a full stop.
207
208 Calling this functions on strings that differ in more than
209 numerals gives undefined results.
210 */
211
212 var targetParts = target.split(NUMBERS);
213 var match;
214 var i = 1;
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,
224 amount));
225 targetParts[i] = part.toFixed(point);
226 i += 2;
227 }
228 return targetParts.join("");
229 }
230
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("");
240 }
241
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);
247 return head + tail;
248 }
249
250 function fastLerp(source, target, amount) {
251 /** Interpolate between two strings based on length
252
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.
256 */
257
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);
264 else
265 return fastLerpBasic(source, target, amount);
266 }
267
268 function lerp(source, target, amount) {
269 /** Interpolate between two strings as best as possible
270
271 If the strings are identical aside from numbers in them,
272 they are passed through numericLerp.
273
274 If the strings are not numbers and short, they are passed
275 through diffLerp.
276
277 Otherwise, they are passed through fastLerp.
278 */
279 source = source.toString();
280 target = target.toString();
281
282 // Fast path for boundary cases.
283 if (amount === 0) return source;
284 if (amount === 1) return target;
285
286 if (areNumericTwins(source, target))
287 return numericLerp(source, target, amount);
288
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;
293
294 var n = source.length * target.length;
295 var appropriate = (n && n < MAX_MATRIX_SIZE) ? diffLerp : fastLerp;
296 return appropriate(source, target, amount);
297 }
298
299 exports.costMatrix = costMatrix;
300 exports.patch = patch;
301 exports.diff = diff;
302 exports.fastLerp = fastLerp;
303 exports.diffLerp = diffLerp;
304 exports.numericLerp = numericLerp;
305 exports.lerp = lerp;
306
307 })(typeof exports === "undefined" ? (this.stringLerp = {}) : exports);