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