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