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