Change how HTML links are handled.
[pwl6.git] / src / yuu / core.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function (yuu) {
8 "use strict";
9
10
11 yuu.require = function (m) {
12 try { return require(m); }
13 catch (exc) { return null; }
14 };
15
16 if (!Math.sign)
17 require("./pre");
18
19 var yT = this.yT || require("./yT");
20 var yf = this.yf || require("./yf");
21 var gui = yuu.require("nw.gui");
22 var fs = yuu.require("fs");
23 var stringLerp = this.stringLerp || yuu.require("string-lerp");
24
25 var initHooks = [];
26 var initOptions = null;
27
28 if (typeof document !== "undefined") {
29 var scripts = document.getElementsByTagName('script');
30 var path = yf.last(scripts).src.split('?')[0];
31 yuu.PATH = path.split('/').slice(0, -1).join('/') + '/';
32 } else {
33 yuu.PATH = "file://" + escape(module.__dirname) + "/";
34 }
35
36 yuu.registerInitHook = initHooks.push.bind(initHooks);
37 /** Register a hook to be called during Yuu initialization
38
39 Hooks are called in registration order with the module and
40 the options dictionary passed to the init method. (This is
41 also set to the module.)
42 */
43
44 function showError (exc, kind) {
45 var prefix = "yuu-" + (kind || "") + "error";
46 yuu.logError(exc);
47 var dialog = document.getElementById(prefix);
48 var errorMessage = document.getElementById(prefix + "-message");
49 if (errorMessage)
50 errorMessage.textContent = exc.message;
51 var errorStack = document.getElementById(prefix + "-stack");
52 if (errorStack)
53 errorStack.textContent = exc.message + "\n\n" + exc.stack;
54 return dialog;
55 }
56 yuu.showError = showError;
57
58 function fatalError (exc) {
59 var dialog = showError(exc, "fatal-");
60 if (dialog)
61 dialog.style.display = "block";
62 if (gui) {
63 gui.Window.get().show();
64 gui.Window.get().focus();
65 }
66 throw exc;
67 }
68
69 yuu.init = function (options) {
70 /** Initialize Yuu and call all registered hooks
71 */
72
73 document.body.className += (navigator.standalone || gui)
74 ? " standalone" : " browser";
75
76 if (gui) {
77 var win = gui.Window.get();
78 var nativeMenuBar = new gui.Menu({ type: "menubar" });
79 if (nativeMenuBar.createMacBuiltin) {
80 nativeMenuBar.createMacBuiltin(
81 document.title, { hideEdit: true });
82 win.menu = nativeMenuBar;
83 }
84 var wkdoc = document;
85 win.on("minimize", function () {
86 var ev = new Event("visibilitychange");
87 wkdoc.hidden = true;
88 wkdoc.dispatchEvent(ev);
89 });
90 win.on("restore", function () {
91 var ev = new Event("visibilitychange");
92 wkdoc.hidden = false;
93 wkdoc.dispatchEvent(ev);
94 });
95 win.on('new-win-policy', function (frame, url, policy) {
96 if (url.startsWith('chrome'))
97 policy.forceNewPopup();
98 else
99 policy.forceDownload();
100 });
101 }
102
103 return new Promise(function (resolve) {
104 // TODO: Some kind of loading progress bar.
105 initOptions = options || {};
106 yuu.log("messages", "Initializing Yuu engine.");
107 var promises = [];
108 // initHooks can be pushed to while iterating, so iterate
109 // by index, not a foreach loop.
110 for (var i = 0; i < initHooks.length; ++i)
111 promises.push(initHooks[i].call(yuu, initOptions));
112 initHooks = null; // Bust future registerInitHook calls.
113 yuu.log("messages", "Initialization hooks complete.");
114 if (gui) {
115 gui.Window.get().show();
116 gui.Window.get().focus();
117 }
118 resolve(Promise.all(yf.filter(null, promises)));
119 }).then(function () {
120 yuu.log("messages", "Loading complete.");
121 }).catch(fatalError);
122 };
123
124 yuu.log = yf.argv(function (category, args) {
125 /** Log a message to the console.
126
127 This supports simple filtering by setting e.g.
128 `yuu.log.errors = true` to log anything with the
129 `"errors"` category.
130 */
131 if (!category || this.log[category]) {
132 switch (category) {
133 case "errors": return console.error.apply(console, args);
134 case "warnings": return console.warn.apply(console, args);
135 default: return console.log.apply(console, args);
136 }
137 }
138 });
139
140 yuu.log.errors = true;
141 yuu.log.warnings = true;
142 yuu.log.messages = true;
143
144 yuu.logError = function (e) {
145 yuu.log("errors", e.message || "unknown error", e);
146 };
147
148 yuu.GET = function (url, params) {
149 /** Promise the HTTP GET the contents of a URL. */
150 return new Promise(function (resolve, reject) {
151 var req = new XMLHttpRequest();
152 req.open("GET", url, true);
153 for (var k in params)
154 req[k] = params[k];
155 req.onload = function () {
156 var status = this.status;
157 // status === 0 is given by node-webkit for success.
158 if ((status >= 200 && status < 300) || status === 0)
159 resolve(this.response);
160 else
161 reject(new Error(
162 url + ": " + status + ": " + this.statusText));
163 };
164 req.onabort = function () { reject(new Error("aborted")); };
165 req.onerror = function () { reject(new Error("network error")); };
166 req.ontimeout = function () { reject(new Error("timed out")); };
167 req.send(null);
168 });
169 };
170
171 yuu.Image = function (src) {
172 /** Promises a DOM Image. */
173 return new Promise(function (resolve, reject) {
174 var img = new Image();
175 img.onload = function () {
176 resolve(img);
177 };
178 img.onerror = function () {
179 var msg = "Unable to load " + img.src;
180 yuu.log("errors", msg);
181 reject(new Error(msg));
182 };
183 img.src = src;
184 });
185 };
186
187 /** Command parsing and execution
188
189 The command API serves several roles. It is a way to enable or
190 disable different game logic within different scenes; capture
191 and replay or automate game events; loosely or late-bind game
192 modules; customize input mappings; and a debugging tool to
193 help inspect or modify the state of a running program.
194
195 A command is a string of a command name followed by arguments
196 separated by whitespace. It's similar to a fully bound
197 closure. It is less flexible but easier to inspect, store,
198 replay, and pass around.
199
200 Command names are mapped to functions, grouped into sets, and
201 the sets pushed onto a stack. They are executed by passing the
202 command string to the execute function which walks the stack
203 looking for the matching command.
204
205 If the command string is prefaced with + or -, true or false
206 are appended to the argument list. e.g. `+command` is
207 equivalent to `command true` and `-command 1 b` is equivalent
208 to `command 1 b false`. By convention, commands of those forms
209 return their internal state when called with neither true or
210 false. This is useful for another special prefix, ++. When
211 called as `++command`, it is executed with no arguments, the
212 result inverted (with !), and then called again passing that
213 inverted value as the last argument.
214 */
215
216 function isCommand (f) {
217 return yf.isFunction(f) && f._isCommandFunction;
218 }
219
220 function cmdbind () {
221 // Commands are practically a subtype of functions. Binding
222 // them (which happens often, e.g. when Scenes register
223 // commands) should also return a command.
224 var f = Function.prototype.bind.apply(this, arguments);
225 // usage is still valid iff no new arguments were given.
226 return cmd(f, arguments.length <= 1 && this.usage, this.description);
227 }
228
229 var cmd = yuu.cmd = yf.argcd(
230 /** Decorate a function for command execution
231
232 Command functions need some special attributes to work
233 correctly. This decorator makes sure they have them.
234 */
235 function (f) { return yuu.cmd(f, null, null); },
236 function (f, description) { return yuu.cmd(f, null, description); },
237 function (f, usage, description) {
238 f._isCommandFunction = true;
239 f.usage = usage || " <value>".repeat(f.length).substring(1);
240 f.description = description || "no description provided";
241 f.bind = cmdbind;
242 return f;
243 }
244 );
245
246 yuu.propcmd = function (o, prop, desc, valspec) {
247 /** Generate a command function that controls a property
248
249 A common pattern for command functions is to simply get or
250 set a single object property. This wrapper will generate a
251 correct function to do that.
252 */
253 valspec = valspec || typeof o[prop];
254 desc = desc || "Retrieve or modify the value of " + prop;
255 return cmd(function () {
256 if (arguments.length)
257 o[prop] = arguments[0];
258 return o[prop];
259 }, "<" + valspec + "?>", desc);
260 };
261
262 var QUOTED_SPLIT = /[^"\s]+|"(?:\\"|[^"])+"/g;
263 var COMMAND_SPLIT = /\s+(&&|\|\||;)\s+/g;
264
265 function parseParam (param) {
266 if (yf.head(param) === "{" && yf.last(param) === "}")
267 return resolvePropertyPath(
268 this, param.substr(1, param.length - 2));
269 try { return JSON.parse(param); }
270 catch (exc) { return param; }
271 }
272
273 function parseCommand (cmdstring, ctx) {
274 /** Parse a command string into an invocation object.
275
276 The command string has a form like `+quux 1 2 3` or
277 `foobar "hello world"`.
278
279 Multiple commands can be joined in one string with &&, ||,
280 or ;. To use these characters literally as a command
281 argument place them in quotes.
282
283 Arguments wrapped in {}s are interpreted as property paths
284 for the provided context object. `{x[0].y}` will resolve
285 `ctx.x[0].y` and put that into the arguments array. To
286 avoid this behavior and get a literal string bounded by
287 {}, JSON-encode the string beforehand (e.g. `"{x[0].y}"`).
288
289 The returned array contains objects with three properties:
290 `name` - the command name to execute
291 `args` - an array of objects to pass as arguments
292 `toggle` - if the command value should be toggled ('++')
293 and pushed into args
294 `cond` - "&&", "||", or ";", indicating what kind of
295 conditional should be applied.
296 */
297
298 var invs = [];
299 var conds = cmdstring.split(COMMAND_SPLIT);
300 for (var i = -1; i < conds.length; i += 2) {
301 var args = conds[i + 1].match(QUOTED_SPLIT).map(parseParam, ctx);
302 var name = args.shift();
303 var toggle = false;
304 if (name[0] === "+" && name[1] === "+") {
305 name = name.substring(2);
306 toggle = true;
307 } else if (name[0] === "+") {
308 name = name.substring(1);
309 args.push(true);
310 } else if (name[0] === "-") {
311 name = name.substring(1);
312 args.push(false);
313 }
314 invs.push({ name: name, args: args, toggle: toggle,
315 cond: conds[i] || ";"});
316 }
317 return invs;
318 }
319
320 yuu.CommandStack = yT({
321 constructor: function () {
322 /** A stack of command sets for command lookup and execution */
323 this._cmdsets = yf.slice(arguments);
324 },
325
326 push: function (cmdset) {
327 /** Add a command set to the lookup stack. */
328 this._cmdsets = this._cmdsets.concat(cmdset);
329 },
330
331 remove: function (cmdset) {
332 /** Remove a command set from the lookup stack. */
333 this._cmdsets = yf.without(this._cmdsets, cmdset);
334 },
335
336 insertBefore: function (cmdset, before) {
337 this._cmdsets = yf.insertBefore(
338 this._cmdsets.slice(), cmdset, before);
339 },
340
341 execute: function (cmdstring, ctx) {
342 /* Execute a command given a command string.
343
344 The command stack is searched top-down for the first
345 command with a matching name, and it is invoked. No
346 other commands are called.
347
348 A command set may also provide a special function named
349 `$`. If no matching command name is found, this
350 function is called with the raw invocation object (the
351 result of yuu.parseCommand) and may return true to stop
352 processing as if the command had been found.
353 */
354 var invs = parseCommand(cmdstring, ctx);
355 var cond;
356 var res;
357 yf.each.call(this, function (inv) {
358 if ((inv.cond === "&&" && !cond) || (inv.cond === "||" && cond))
359 return;
360 if (!yf.eachrUntil(function (cmdset) {
361 var cmd = cmdset[inv.name];
362 if (cmd) {
363 if (inv.toggle)
364 inv.args.push(!cmd.apply(null, inv.args));
365 yuu.log("commands", "Executing:", inv.name,
366 inv.args.map(JSON.stringify).join(" "));
367 res = cmd.apply(null, inv.args);
368 cond = res === undefined ? cond : !!res;
369 yuu.log("commands", "Result:", JSON.stringify(res));
370 return true;
371 }
372 return cmdset.$ && cmdset.$(inv);
373 }, this._cmdsets))
374 yuu.log("errors", "Unknown command", inv.name);
375 }, invs);
376 return res;
377 }
378 });
379
380 yuu.extractCommands = function (object) {
381 var commands = {};
382 yf.each(function (prop) {
383 // Check the descriptor before checking the value, because
384 // checking the value of accessors (which should never be
385 // stable commands) is generally a bad idea during
386 // constructors, and command sets are often filled in during
387 // constructors.
388 if (yT.isDataDescriptor(yT.getPropertyDescriptor(object, prop))
389 && isCommand(object[prop]))
390 commands[prop] = object[prop].bind(object);
391 }, yf.allKeys(object));
392 return commands;
393 };
394
395 yuu.commandStack = new yuu.CommandStack(yuu.defaultCommands = {
396 /** The default command stack and set. */
397 cmds: yuu.cmd(function (term) {
398 term = term || "";
399 var cmds = [];
400 yuu.commandStack._cmdsets.forEach(function (cmdset) {
401 for (var cmdname in cmdset) {
402 if (cmdname.indexOf(term) >= 0) {
403 var cmd = cmdset[cmdname];
404 var msg;
405 if (cmd.usage)
406 msg = [cmdname, cmd.usage, "--", cmd.description];
407 else
408 msg = [cmdname, "--", cmd.description];
409 cmds.push(msg.join(" "));
410 }
411 }
412 });
413 yuu.log("messages", cmds.join("\n"));
414 }, "<term?>", "display available commands (matching the term)"),
415
416 echo: yuu.cmd(function () {
417 yuu.log("messages", arguments);
418 }, "...", "echo arguments to the console"),
419
420 log: yuu.cmd(function (name, state) {
421 if (state !== undefined)
422 yuu.log[name] = !!state;
423 return yuu.log[name];
424 }, "<category> <boolean?>", "enable/disable a logging category")
425
426 });
427
428 yuu.defaultCommands.showDevTools = yuu.cmd(function () {
429 if (gui)
430 gui.Window.get().showDevTools();
431 }, "show developer tools");
432
433 yuu.anchorPoint = function (anchor, x0, y0, x1, y1) {
434 /** Calculate the anchor point for a box given extents and anchor mode
435
436 This function is the inverse of yuu.bottomLeft.
437 */
438 switch (anchor) {
439 case "center": return [(x0 + x1) / 2, (y0 + y1) / 2];
440 case "top": return [(x0 + x1) / 2, y1];
441 case "bottom": return [(x0 + x1) / 2, y0];
442 case "left": return [x0, (y0 + y1) / 2];
443 case "right": return [x1, (y0 + y1) / 2];
444
445 case "topleft": return [x0, y1];
446 case "topright": return [x1, y1];
447 case "bottomleft": return [x0, y0];
448 case "bottomright": return [x0, y0];
449 default: return [anchor[0], anchor[1]];
450 }
451 };
452
453 yuu.bottomLeft = function (anchor, x, y, w, h) {
454 /** Calculate the bottom-left for a box given size and anchor mode
455
456 This function is the inverse of yuu.anchorPoint.
457 */
458 switch (anchor) {
459 case "center": return [x - w / 2, y - h / 2];
460 case "top": return [x - w / 2, y - h];
461 case "bottom": return [x - w / 2, y];
462 case "left": return [x, y - h / 2];
463 case "right": return [x - w, y - h / 2];
464
465 case "topleft": return [x, y - h];
466 case "topright": return [x - w, y - h];
467 case "bottomleft": return [x, y];
468 case "bottomright": return [x - w, y];
469 default: return [anchor[0], anchor[1]];
470 }
471 };
472
473 yuu.lerp = function (a, b, p) {
474 return (a !== null && a !== undefined && a.lerp)
475 ? a.lerp(b, p) : (b !== null && b !== undefined && b.lerp)
476 ? b.lerp(a, 1 - p) : p < 0.5 ? a : b;
477 };
478
479 yuu.bilerp = function (x0y0, x1y0, x0y1, x1y1, px, py) {
480 /** Bilinearly interpolate between four values in two dimensions */
481 return yuu.lerp(yuu.lerp(x0y0, x1y0, px), yuu.lerp(x0y1, x1y1, px), py);
482 };
483
484 function resolvePropertyPath (object, path) {
485 /** Look up a full property path
486
487 If a null is encountered in the path, this function returns
488 null. If undefined is encountered or a property is missing, it
489 returns undefined.
490 */
491 var parts = path.replace(/\[(\w+)\]/g, '.$1').split('.');
492 for (var i = 0;
493 i < parts.length && object !== undefined && object !== null;
494 ++i) {
495 object = object[parts[i]];
496 }
497 return object;
498 }
499
500 yuu.Random = yT({
501 /** Somewhat like Python's random.Random.
502
503 Passed a function that returns a uniform random variable in
504 [0, 1) it can do other useful randomization algorithms.
505
506 Its methods are implemented straightforwardly rather than
507 rigorously - this means they may not behave correctly in
508 common edge cases like precision loss.
509 */
510 constructor: function (generator) {
511 this.random = generator || Math.random;
512 this._spareGauss = null;
513 },
514
515 choice: function (seq) {
516 /** Return a random element from the provided array. */
517 return seq[this.randrange(0, seq.length)];
518 },
519
520 randrange: yf.argcd(
521 function (a) {
522 /** Return a uniform random integer in [0, a). */
523 return (this.random() * a) | 0;
524 },
525
526 function (a, b) {
527 /** Return a uniform random integer in [a, b). */
528 a = a | 0;
529 b = b | 0;
530 return a + ((this.random() * (b - a)) | 0);
531 },
532
533 function (a, b, step) {
534 /** Return a uniform random number in [a, b).
535
536 The number is constrained to values of a + i * step
537 where i is a non-negative integer.
538 */
539 var i = Math.ceil((b - a) / step);
540 return a + this.randrange(i) * step;
541 }
542 ),
543
544 uniform: yf.argcd(
545 function (a) {
546 /** Return a uniform random variable in [0, a). */
547 return a * this.random();
548 },
549 function (a, b) {
550 /** Return a uniform random variable in [a, b). */
551 return a + (b - a) * this.random();
552 }
553 ),
554
555 gauss: function (mean, sigma) {
556 var u = this._spareGauss, v, s;
557 this._spareGauss = null;
558
559 if (u === null) {
560 do {
561 u = this.uniform(-1, 1);
562 v = this.uniform(-1, 1);
563 s = u * u + v * v;
564 } while (s >= 1.0 || s === 0.0);
565 var t = Math.sqrt(-2.0 * Math.log(s) / s);
566 this._spareGauss = v * t;
567 u *= t;
568 }
569 return mean + sigma * u;
570 },
571
572 randbool: yf.argcd(
573 /** Return true the given percent of the time (default 50%). */
574 function () { return this.random() < 0.5; },
575 function (a) { return this.random() < a; }
576 ),
577
578 randsign: function (v) {
579 return this.randbool() ? v : -v;
580 },
581
582 shuffle: function (seq) {
583 for (var i = seq.length - 1; i > 0; --i) {
584 var index = this.randrange(i + 1);
585 var temp = seq[i];
586 seq[i] = seq[index];
587 seq[index] = temp;
588 }
589 return seq;
590 },
591
592 discard: function (z) {
593 z = z | 0;
594 while (z-- > 0)
595 this.random();
596 }
597 });
598
599 yuu.createLCG = yf.argcd(
600 /** Linear congruential random generator
601
602 This returns a function that generates numbers [0, 1) as
603 with Math.random. You can also read or assign the `state`
604 attribute to set the internal state.
605 */
606 function () { return yuu.createLCG(Math.random() * 2147483647); },
607 function (seed) {
608 var state = seed | 0;
609 return function generator () {
610 state = (state * 1664525 + 1013904223) % 4294967296;
611 return state / 4294967296;
612 };
613 }
614 );
615
616 yuu.random = new yuu.Random();
617
618 function defaultKey (args) {
619 // Cache things that can be constructed with one string.
620 return args.length === 1 && yf.isString(args[0]) ? args[0] : null;
621 }
622
623 yuu.Caching = function (Type, cacheKey) {
624 function ctor () {
625 var k = ctor._cacheKey(arguments);
626 var o = k && ctor._cache[k];
627 if (!o)
628 o = ctor._cache[k] = yf.construct(ctor.Uncached, arguments);
629 return o;
630 }
631 ctor._cacheKey = cacheKey || defaultKey;
632 ctor._cache = {};
633 ctor.Uncached = Type;
634 return ctor;
635 };
636
637 yuu.transpose2d = function (a) {
638 for (var x = 0; x < a.length; ++x) {
639 for (var y = 0; y < x; ++y) {
640 var t = a[x][y];
641 a[x][y] = a[y][x];
642 a[y][x] = t;
643 }
644 }
645 };
646
647 yuu.normalizeRadians = function (theta) {
648 var PI = Math.PI;
649 return (theta + 3 * PI) % (2 * PI) - PI;
650 };
651
652 yuu.radians = function (v) {
653 return v * (Math.PI / 180.0);
654 };
655
656 yuu.degrees = function (v) {
657 return v * (180.0 / Math.PI);
658 };
659
660 var SHORT = /(\/|^)@(.+)$/;
661 yuu.resourcePath = function (path, category, ext) {
662 var match;
663 if ((match = path.match(SHORT))) {
664 path = path.replace(/^yuu\/@/, yuu.PATH + "@")
665 .replace(SHORT, "$1data/" + category + "/$2");
666 if (match[2].indexOf(".") === -1)
667 path += "." + ext;
668 }
669 return path;
670 };
671
672 yuu.ready = function (resources, result) {
673 return Promise.all(yf.filter(null, yf.pluck("ready", resources)))
674 .then(yf.K(result));
675 };
676
677 yuu.openURL = function (url) {
678 if (gui && gui.Shell)
679 gui.Shell.openExternal(url);
680 else
681 window.open(url);
682 };
683
684 function crossPlatformFilename (basename) {
685 return basename
686 // Replace D/M/Y with D-M-Y, and H:M:S with H.M.S.
687 .replace(/\//g, "-").replace(/:/g, ".")
688 // Replace all other problematic characters with _.
689 .replace(/["<>*?|\\]/g, "_");
690 }
691
692 yuu.downloadURL = function (url, suggestedName) {
693 var regex = /^data:[^;+]+;base64,(.*)$/;
694 var matches = url.match(regex);
695 suggestedName = crossPlatformFilename(suggestedName);
696 if (matches && fs) {
697 var data = matches[1];
698 var buffer = new Buffer(data, 'base64');
699 var HOME = process.env.HOME
700 || process.env.HOMEPATH
701 || process.env.USERPROFILE;
702 var filename = HOME + "/" + suggestedName;
703 console.log("Saving to", filename);
704 fs.writeFileSync(filename, buffer);
705 } else {
706 var link = document.createElement('a');
707 link.style.display = "none";
708 link.href = url;
709 link.download = suggestedName;
710 // Firefox (as of 28) won't download from a link not rooted in
711 // the document; so, root it and then remove it when done.
712 document.body.appendChild(link);
713 link.click();
714 document.body.removeChild(link);
715 }
716 };
717
718 yuu.AABB = yT({
719 constructor: yf.argcd(
720 function () { this.constructor(0, 0, 0, 0); },
721 function (w, h) { this.constructor(0, 0, w, h); },
722 function (x0, y0, x1, y1) {
723 this.x0 = x0;
724 this.y0 = y0;
725 this.x1 = x1;
726 this.y1 = y1;
727 }
728 ),
729
730 w: {
731 get: function () { return this.x1 - this.x0; },
732 set: function (w) { this.x1 = this.x0 + w; }
733 },
734
735 h: {
736 get: function () { return this.y1 - this.y0; },
737 set: function (h) { this.y1 = this.y0 + h; }
738 },
739
740 size: { swizzle: "wh" },
741
742 contains: yf.argcd(
743 function (p) { return this.contains(p.x, p.y); },
744 function (x, y) {
745 return x >= this.x0 && x < this.x1
746 && y >= this.y0 && y < this.y1;
747 }
748 ),
749
750 matchAspectRatio: function (outer) {
751 var matched = new this.constructor(
752 this.x0, this.y0, this.x1, this.y1);
753 var aRatio = matched.w / matched.h;
754 var bRatio = outer.w / outer.h;
755 if (aRatio > bRatio) {
756 // too wide, must be taller
757 var h = matched.w / bRatio;
758 var dh = h - matched.h;
759 matched.y0 -= dh / 2;
760 matched.y1 += dh / 2;
761 } else {
762 // too tall, must be wider
763 var w = matched.h * bRatio;
764 var dw = w - matched.w;
765 matched.x0 -= dw / 2;
766 matched.x1 += dw / 2;
767 }
768 return matched;
769 },
770
771 alignedInside: function (outer, alignment) {
772 var x0, y0;
773 switch (alignment) {
774 case "bottomleft":
775 x0 = outer.x0;
776 y0 = outer.y0;
777 break;
778 case "bottom":
779 x0 = outer.x0 + (outer.w - this.w) / 2;
780 y0 = outer.y0;
781 break;
782 case "bottomright":
783 x0 = outer.x0 - this.w;
784 y0 = outer.y0;
785 break;
786 case "left":
787 x0 = outer.x0;
788 y0 = outer.x0 + (outer.h - this.h) / 2;
789 break;
790 case "center":
791 x0 = outer.x0 + (outer.w - this.w) / 2;
792 y0 = outer.x0 + (outer.h - this.h) / 2;
793 break;
794 case "right":
795 x0 = outer.x1 - this.w;
796 y0 = outer.x0 + (outer.h - this.h) / 2;
797 break;
798 case "topleft":
799 x0 = outer.x0;
800 y0 = outer.y1 - this.h;
801 break;
802 case "top":
803 x0 = outer.x0 + (outer.w - this.w) / 2;
804 y0 = outer.y1 - this.h;
805 break;
806 case "topright":
807 x0 = outer.x1 - this.w;
808 y0 = outer.y1 - this.h;
809 break;
810 }
811 return new this.constructor(x0, y0, x0 + this.w, y0 + this.h);
812 }
813 });
814
815 function splitPathExtension (path) {
816 var dot = path.lastIndexOf(".");
817 if (dot <= 0) return [path, ""];
818
819 var dir = path.lastIndexOf("/");
820 if (dot < dir) return [path, ""];
821
822 return [path.substring(0, dot), path.substring(dot)];
823 }
824 yuu.splitPathExtension = splitPathExtension;
825
826 if (stringLerp) {
827 yT.defineProperty(String.prototype, "lerp", function (b, p) {
828 b = b.toString();
829 // Never numericLerp - if that's desired force Numbers.
830 // Be more conservative than stringLerp since this runs
831 // often and the diff can't be easily hoisted.
832 return this.length * b.length > 256
833 ? stringLerp.fastLerp(this, b, p)
834 : stringLerp.diffLerp(this, b, p);
835 });
836 }
837
838 yT.defineProperties(Number.prototype, {
839 lerp: function (b, p) { return this + (b - this) * p; }
840 });
841
842 yT.defineProperties(Array.prototype, {
843 lerp: function (b, p) {
844 var length = Math.round(this.length.lerp(b.length, p));
845 var c = new this.constructor(length);
846 for (var i = 0; i < length; ++i) {
847 if (i >= this.length)
848 c[i] = b[i];
849 else if (i >= b.length)
850 c[i] = this[i];
851 else
852 c[i] = this[i].lerp(b[i], p);
853 }
854 return c;
855 }
856 });
857
858 /** Typed array extensions
859
860 https://www.khronos.org/registry/typedarray/specs/1.0/
861 BUT: Read on for fun times in browser land~
862
863 Ideally we could just set these once on ArrayBufferView, but
864 the typed array specification doesn't require that such a
865 constructor actually exist. And in Firefox (18), it doesn't.
866
867 More infurating, in Safari (7.0.3) Int8Array etc. are not
868 functions so this needs to be added to the prototype
869 directly. This is a violation of the specification which
870 requires such constructors, and ECMA which requires
871 constructors be functions, and common decency.
872 */
873
874 [ Float32Array, Float64Array, Int8Array, Uint8Array,
875 Int16Array, Uint16Array, Int32Array, Uint32Array
876 ].forEach(function (A) {
877 yT.defineProperties(A.prototype, {
878 slice: yf.argcd(
879 /** Like Array's slice, but for typed arrays */
880 function () { return new this.constructor(this); },
881 function (begin) {
882 return new this.constructor(this.subarray(begin));
883 },
884 function (begin, end) {
885 return new this.constructor(this.subarray(begin, end));
886 }
887 ),
888
889 fill: Array.prototype.fill,
890 reverse: Array.prototype.reverse,
891
892 lerp: function (b, p) {
893 if (p === 0)
894 return this.slice();
895 else if (p === 1)
896 return b.slice();
897 var c = new this.constructor(this.length);
898 for (var i = 0; i < this.length; ++i)
899 c[i] = this[i] + (b[i] - this[i]) * p;
900 return c;
901 }
902 });
903 });
904
905 }).call(typeof exports === "undefined" ? this : exports,
906 typeof exports === "undefined" ? (this.yuu = {}) : exports);