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