Allow empty attribute syntax to mean "same command as ID".
[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 hw: { get: function () { return this.w / 2; } },
744 hh: { get: function () { return this.h / 2; } },
745
746 contains: yf.argcd(
747 function (p) { return this.contains(p.x, p.y); },
748 function (x, y) {
749 return x >= this.x0 && x < this.x1
750 && y >= this.y0 && y < this.y1;
751 }
752 ),
753
754 matchAspectRatio: function (outer) {
755 var matched = new this.constructor(
756 this.x0, this.y0, this.x1, this.y1);
757 var aRatio = matched.w / matched.h;
758 var bRatio = outer.w / outer.h;
759 if (aRatio > bRatio) {
760 // too wide, must be taller
761 var h = matched.w / bRatio;
762 var dh = h - matched.h;
763 matched.y0 -= dh / 2;
764 matched.y1 += dh / 2;
765 } else {
766 // too tall, must be wider
767 var w = matched.h * bRatio;
768 var dw = w - matched.w;
769 matched.x0 -= dw / 2;
770 matched.x1 += dw / 2;
771 }
772 return matched;
773 },
774
775 alignedInside: function (outer, alignment) {
776 var x0, y0;
777 switch (alignment) {
778 case "bottomleft":
779 x0 = outer.x0;
780 y0 = outer.y0;
781 break;
782 case "bottom":
783 x0 = outer.x0 + (outer.w - this.w) / 2;
784 y0 = outer.y0;
785 break;
786 case "bottomright":
787 x0 = outer.x0 - this.w;
788 y0 = outer.y0;
789 break;
790 case "left":
791 x0 = outer.x0;
792 y0 = outer.x0 + (outer.h - this.h) / 2;
793 break;
794 case "center":
795 x0 = outer.x0 + (outer.w - this.w) / 2;
796 y0 = outer.x0 + (outer.h - this.h) / 2;
797 break;
798 case "right":
799 x0 = outer.x1 - this.w;
800 y0 = outer.x0 + (outer.h - this.h) / 2;
801 break;
802 case "topleft":
803 x0 = outer.x0;
804 y0 = outer.y1 - this.h;
805 break;
806 case "top":
807 x0 = outer.x0 + (outer.w - this.w) / 2;
808 y0 = outer.y1 - this.h;
809 break;
810 case "topright":
811 x0 = outer.x1 - this.w;
812 y0 = outer.y1 - this.h;
813 break;
814 }
815 return new this.constructor(x0, y0, x0 + this.w, y0 + this.h);
816 }
817 });
818
819 function splitPathExtension (path) {
820 var dot = path.lastIndexOf(".");
821 if (dot <= 0) return [path, ""];
822
823 var dir = path.lastIndexOf("/");
824 if (dot < dir) return [path, ""];
825
826 return [path.substring(0, dot), path.substring(dot)];
827 }
828 yuu.splitPathExtension = splitPathExtension;
829
830 if (stringLerp) {
831 yT.defineProperty(String.prototype, "lerp", function (b, p) {
832 b = b.toString();
833 // Never numericLerp - if that's desired force Numbers.
834 // Be more conservative than stringLerp since this runs
835 // often and the diff can't be easily hoisted.
836 return this.length * b.length > 256
837 ? stringLerp.fastLerp(this, b, p)
838 : stringLerp.diffLerp(this, b, p);
839 });
840 }
841
842 yT.defineProperties(Number.prototype, {
843 lerp: function (b, p) { return this + (b - this) * p; }
844 });
845
846 yT.defineProperties(Array.prototype, {
847 lerp: function (b, p) {
848 var length = Math.round(this.length.lerp(b.length, p));
849 var c = new this.constructor(length);
850 for (var i = 0; i < length; ++i) {
851 if (i >= this.length)
852 c[i] = b[i];
853 else if (i >= b.length)
854 c[i] = this[i];
855 else
856 c[i] = this[i].lerp(b[i], p);
857 }
858 return c;
859 }
860 });
861
862 /** Typed array extensions
863
864 https://www.khronos.org/registry/typedarray/specs/1.0/
865 BUT: Read on for fun times in browser land~
866
867 Ideally we could just set these once on ArrayBufferView, but
868 the typed array specification doesn't require that such a
869 constructor actually exist. And in Firefox (18), it doesn't.
870
871 More infurating, in Safari (7.0.3) Int8Array etc. are not
872 functions so this needs to be added to the prototype
873 directly. This is a violation of the specification which
874 requires such constructors, and ECMA which requires
875 constructors be functions, and common decency.
876 */
877
878 [ Float32Array, Float64Array, Int8Array, Uint8Array,
879 Int16Array, Uint16Array, Int32Array, Uint32Array
880 ].forEach(function (A) {
881 yT.defineProperties(A.prototype, {
882 slice: yf.argcd(
883 /** Like Array's slice, but for typed arrays */
884 function () { return new this.constructor(this); },
885 function (begin) {
886 return new this.constructor(this.subarray(begin));
887 },
888 function (begin, end) {
889 return new this.constructor(this.subarray(begin, end));
890 }
891 ),
892
893 fill: Array.prototype.fill,
894 reverse: Array.prototype.reverse,
895
896 lerp: function (b, p) {
897 if (p === 0)
898 return this.slice();
899 else if (p === 1)
900 return b.slice();
901 var c = new this.constructor(this.length);
902 for (var i = 0; i < this.length; ++i)
903 c[i] = this[i] + (b[i] - this[i]) * p;
904 return c;
905 }
906 });
907 });
908
909 }).call(typeof exports === "undefined" ? this : exports,
910 typeof exports === "undefined" ? (this.yuu = {}) : exports);