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