Initial import.
authorJoe Wreschnig <joe.wreschnig@gmail.com>
Thu, 4 Sep 2014 19:48:29 +0000 (21:48 +0200)
committerJoe Wreschnig <joe.wreschnig@gmail.com>
Thu, 4 Sep 2014 19:48:29 +0000 (21:48 +0200)
92 files changed:
.gitattributes [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.projectile [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
rules/git.mk [new file with mode: 0644]
rules/icons.mk [new file with mode: 0644]
rules/javascript.mk [new file with mode: 0644]
rules/node-webkit.mk [new file with mode: 0644]
rules/pngcrush.mk [new file with mode: 0644]
rules/programs.mk [new file with mode: 0644]
src/data/images/.gitignore [new file with mode: 0644]
src/data/images/book.xcf.gz [new file with mode: 0644]
src/data/images/circle-inner.png [new file with mode: 0644]
src/data/images/circle-outer-ee.png [new file with mode: 0644]
src/data/images/circle-outer.png [new file with mode: 0644]
src/data/images/circle-rim.png [new file with mode: 0644]
src/data/images/circle.xcf.gz [new file with mode: 0644]
src/data/images/hand.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_128x128.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_128x128@2x.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_16x16.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_256x256.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_32x32.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_32x32@2x.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_64x64.png [new file with mode: 0644]
src/data/images/icons.iconset/icon_64x64@2x.png [new file with mode: 0644]
src/data/images/sigils.png [new file with mode: 0644]
src/data/license.txt [new file with mode: 0644]
src/data/shaders/noise.glsl [new file with mode: 0644]
src/data/shaders/noisyblocks.frag [new file with mode: 0644]
src/data/shaders/noisyquads.vert [new file with mode: 0644]
src/data/sound/book-appear.wav [new file with mode: 0644]
src/data/sound/book-dismiss.wav [new file with mode: 0644]
src/data/sound/click-1.wav [new file with mode: 0644]
src/data/sound/click-2.wav [new file with mode: 0644]
src/data/sound/clicking.wav [new file with mode: 0644]
src/data/sound/page-turn-1.wav [new file with mode: 0644]
src/data/sound/page-turn-2.wav [new file with mode: 0644]
src/data/sound/page-turn-3.wav [new file with mode: 0644]
src/data/sound/regear.wav [new file with mode: 0644]
src/data/sound/slam.wav [new file with mode: 0644]
src/data/sound/switch.wav [new file with mode: 0644]
src/data/sound/tick.wav [new file with mode: 0644]
src/data/sound/tock.wav [new file with mode: 0644]
src/data/sound/winding.wav [new file with mode: 0644]
src/ext/FiraMono-Bold.woff [new file with mode: 0644]
src/ext/FiraMono-Regular.woff [new file with mode: 0644]
src/ext/FiraSans-Bold.woff [new file with mode: 0644]
src/ext/FiraSans-BoldItalic.woff [new file with mode: 0644]
src/ext/FiraSans-Italic.woff [new file with mode: 0644]
src/ext/FiraSans-Regular.woff [new file with mode: 0644]
src/ext/FiraSans-UltraLight.woff [new file with mode: 0644]
src/ext/FiraSans-UltraLightItalic.woff [new file with mode: 0644]
src/ext/font-awesome.woff [new file with mode: 0644]
src/ext/gamepad.js [new file with mode: 0644]
src/ext/gl-matrix.js [new file with mode: 0644]
src/ext/hammer.js [new file with mode: 0644]
src/ext/string-lerp.js [new file with mode: 0644]
src/index.html [new file with mode: 0644]
src/pwl6.css [new file with mode: 0644]
src/pwl6.js [new file with mode: 0644]
src/yuu/audio.js [new file with mode: 0644]
src/yuu/ce.js [new file with mode: 0644]
src/yuu/core.js [new file with mode: 0644]
src/yuu/data/license.txt [new file with mode: 0644]
src/yuu/data/shaders/color.frag [new file with mode: 0644]
src/yuu/data/shaders/default.frag [new file with mode: 0644]
src/yuu/data/shaders/default.vert [new file with mode: 0644]
src/yuu/data/yuu.css [new file with mode: 0644]
src/yuu/director.js [new file with mode: 0644]
src/yuu/gfx.js [new file with mode: 0644]
src/yuu/input.js [new file with mode: 0644]
src/yuu/pre.js [new file with mode: 0644]
src/yuu/rdr.js [new file with mode: 0644]
src/yuu/storage.js [new file with mode: 0644]
src/yuu/yT.js [new file with mode: 0644]
src/yuu/yf.js [new file with mode: 0644]
test/jshint.config [new file with mode: 0644]
test/spec/yuu/core.js [new file with mode: 0644]
test/spec/yuu/input.js [new file with mode: 0644]
test/spec/yuu/storage.js [new file with mode: 0644]
test/spec/yuu/yT.js [new file with mode: 0644]
test/spec/yuu/yf.js [new file with mode: 0644]
tools/LICENSE.rcedit [new file with mode: 0644]
tools/composer/composer.js [new file with mode: 0644]
tools/composer/index.html [new file with mode: 0644]
tools/generate-appcache [new file with mode: 0755]
tools/generate-nw [new file with mode: 0755]
tools/generate-osx-app [new file with mode: 0755]
tools/nw-linux-wrapper [new file with mode: 0755]
tools/rcedit.exe [new file with mode: 0644]

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..5966153
--- /dev/null
@@ -0,0 +1,2 @@
+.gitattributes export-ignore
+.gitignore export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..17918bb
--- /dev/null
@@ -0,0 +1,7 @@
+*~
+\#*
+.DS_Store
+node_modules
+build
+node-webkit
+
diff --git a/.projectile b/.projectile
new file mode 100644 (file)
index 0000000..7865c8b
--- /dev/null
@@ -0,0 +1,2 @@
+-/node_modules
+-/node-webkit
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..06f7e3f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,102 @@
+#!/usr/bin/make -f
+
+node-webkit-version := 0.10.2
+node-webkit-version-win-ia32.zip := 0.8.6
+
+.DELETE_ON_ERROR:
+include rules/programs.mk
+include rules/javascript.mk
+include rules/icons.mk
+include rules/git.mk
+include rules/node-webkit.mk
+include rules/pngcrush.mk
+
+.PHONY: all check distclean lint test dist clean serve
+
+APPLICATION := pwl6
+DISTDIR := build/dist
+VERSION := $(call git-describe)
+DISTROOT := $(DISTDIR)/$(APPLICATION)-$(VERSION)
+
+FIND_FILES := -type f ! -name '.*'
+FIND_JS := -type f -name '*.js'
+
+SOURCES := $(shell cd src && find . $(FIND_FILES))
+MY_SOURCES := $(shell cd src && find . $(FIND_JS) -not -path './ext/*')
+
+TEST_SOURCES := $(shell find test/spec $(FIND_JS))
+
+LINT_TARGETS := $(call jshint-stampify,$(MY_SOURCES))
+TEST_TARGETS := $(call jstest-stampify,$(TEST_SOURCES))
+
+JSHINTCONFIG := test/jshint.config
+JSTEST_NODE_PATH := src
+VPATH := src
+
+ICONSETS := $(shell find src -type d -name '*.iconset')
+ICONS := $(ICONSETS:.iconset=.icns) $(ICONSETS:.iconset=.ico)
+
+IMAGEGZSRC := $(shell find src -type f -name '*.xcf.gz')
+IMAGESRC := $(shell find src -type f -name '*.xcf')
+IMAGES := $(IMAGEGZSRC:.xcf.gz=.png) $(IMAGESRC:.xcf=.png)
+
+BUILT := $(ICONS) $(IMAGES)
+
+HTTP_SERVER_PORT ?= 8000
+
+all: check $(BUILT) $(call pngcrush-stampify,$(IMAGES))
+
+$(DISTDIR):
+       mkdir -p $@
+
+dist: $(addprefix $(DISTROOT),-src.zip -src.tar.gz .appcache .nw -osx-ia32.zip -osx-x64.zip -linux-ia32.tar.gz -linux-x64.tar.gz -win-ia32.zip)
+
+test/spec/%.js: %.js
+       touch $@
+
+lint: $(LINT_TARGETS)
+
+test: $(TEST_TARGETS)
+
+check: lint test
+
+serve: | $(npmbindir)/http-server
+       $(npmbindir)/http-server $(@D) -p $(HTTP_SERVER_PORT) -c-1
+
+clean:
+       $(RM) $(IMAGES)
+       $(RM) $(ICONS)
+       $(RM) -r build
+
+distclean: clean
+       $(RM) -r node_modules
+       $(RM) $(node-webkit-archives)
+
+$(DISTROOT)-src.zip $(DISTROOT)-src.tar.gz: | .git
+       mkdir -p $(@D)
+       $(call git-archive,$@,$(notdir $(DISTROOT))/)
+
+$(DISTROOT).bare.zip: | .git
+       $(RM) $@
+       $(RM) -r $@.tmp
+       mkdir -p $@.tmp
+       cd src && $(GIT) archive $(call git-describe) . | tar -x -C ../$@.tmp
+       $(MAKE) $(BUILT:src/%=$@.tmp/%)
+       $(RM) $(IMAGESRC:src/%=$@.tmp/%) $(IMAGEGZSRC:src/%=$@.tmp/%)
+       cd $@.tmp && $(ZIP) ../$(@F) -r .
+       $(RM) -r $@.tmp
+
+%.appcache: %.bare.zip tools/generate-appcache
+       $(RM) -r $@.tmp $@
+       $(UNZIP) -d $@.tmp $<
+       tools/generate-appcache $@.tmp
+       mv $@.tmp $@
+
+# Python's zipfile module generates zipfiles that node-webkit cannot
+# read, so delegate to a real zip tool.
+%.nw: %.bare.zip tools/generate-nw
+       $(RM) -r $@ $@.tmp
+       $(UNZIP) -d $@.tmp $<
+       tools/generate-nw $@.tmp
+       cd $@.tmp && $(ZIP) -r ../$(@F) .
+       $(RM) -r $@.tmp
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..c949cec
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# Yuu - a WebGL game library
+
+Software developers do their best now and are preparing. Please wait
+warmly until it is ready.
+
+* * *
+
+Here are some reasons to avoid this library for now:
+
+* There's no real documentation. There's no API stability promises.
+* No one has actually made a game in it.
+* There's no draw batching. Each quad is its own draw call.
+* In fact, there's been no optimization in general.
+* Before I wrote this I'd never once used the JavaScript `new` keyword.
diff --git a/rules/git.mk b/rules/git.mk
new file mode 100644 (file)
index 0000000..e8ceb78
--- /dev/null
@@ -0,0 +1,13 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+GIT ?= git
+git-describe = $(shell $(GIT) describe --tags --always $1)
+
+define git-archive
+$(GIT) archive --output '$1' $(if $2,--prefix '$2') '$(call git-describe,$3)'
+endef
+
+.git:
+       $(error "This target must be run inside a git repository.")
diff --git a/rules/icons.mk b/rules/icons.mk
new file mode 100644 (file)
index 0000000..eba5e89
--- /dev/null
@@ -0,0 +1,39 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+.DELETE_ON_ERROR:
+
+XCF2PNG ?= xcf2png
+
+%.png: %.xcf
+       $(XCF2PNG) $< > $@
+
+# First: xcf2png by default calls zcat rather than gzcat. This is
+# totally always broken; zcat forces a .Z extension on its input
+# filename. So we can't rely on xcf2png's default behavior. But it
+# offers -Z for a custom decompression program.
+#
+# BUT: Gimp produces gz files with some padding zeros because, I don't
+# know, someone might want to save their compressed xcfs to DECtape.
+# gzip has a -q option to not *print* the warning associated with this
+# harmless thing, but then goes ahead and exits non-zero anyway, which
+# makes xcf2png barf and die before writing anything even though it
+# got perfectly good data.
+#
+# So: 1) use gunzip, 2) manually feed it to xcf2png, 3) hope nothing is
+# set to die based on pipe status, 4) hope that if the xcf data is
+# actually busted xcf2png will do something helpful.
+%.png: %.xcf.gz
+       gunzip -c $< | $(XCF2PNG) - > $@
+
+.SECONDEXPANSION:
+
+ICONUTIL ?= $(firstword $(shell command -v iconutil icnsutil) iconutil)
+
+%.icns: %.iconset $$(wildcard $$(@D)/$$*.iconset/icon_*.png)
+       $(ICONUTIL) -c icns -o $@ $<
+
+%.ico: %.iconset $$(wildcard $$(@D)/$$*.iconset/icon_*[0-9].png)
+       convert -background transparent -colors 256 $(filter-out $<,$^) $@
+
diff --git a/rules/javascript.mk b/rules/javascript.mk
new file mode 100644 (file)
index 0000000..15fb8a5
--- /dev/null
@@ -0,0 +1,62 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+.DELETE_ON_ERROR:
+
+javascript>fallback = $(firstword $(shell command -v $1) $2 $1)
+
+NPM ?= npm
+NPMROOT ?= $(CURDIR)
+
+npmbindir = $(NPMROOT)/node_modules/.bin
+.PRECIOUS: $(npmbindir)/%
+
+npmbin = $(call javascript>fallback,$1,$(npmbindir)/$1)
+
+JSTEST ?= $(npmbindir)/jstest
+JSHINT ?= $(call npmbin,jshint)
+
+uglifyjs_npm_package := uglify-js
+
+UGLIFY ?= $(call npmbin,uglifyjs)
+UGLIFYFLAGS ?= --comments \
+               --compress $(UGLIFYCOMPRESSFLAGS) \
+               --mangle $(UGLIFYMANGLEFLAGS)
+
+BUILDDIR ?= build/
+JSSTAMPDIR ?= $(BUILDDIR)/stamp
+JSHINTDIR ?= $(JSSTAMPDIR)
+JSTESTDIR ?= $(JSSTAMPDIR)
+JSUGLYDIR ?= $(CURDIR)
+
+JSHINTFLAGS += $(if $(JSHINTCONFIG),--config $(JSHINTCONFIG))
+JSTESTFLAGS += $(if $(JSTESTFORMAT),--format $(JSTESTFORMAT))
+JSTESTENV += $(if $(JSTEST_NODE_PATH),NODE_PATH=$(JSTEST_NODE_PATH))
+JSTESTFORMAT ?= spec
+
+jshint-stampify = $(patsubst %.js,$(JSHINTDIR)/%.js.lint,$1)
+jstest-stampify = $(patsubst %.js,$(JSTESTDIR)/%.js.test,$1)
+uglify-stampify = $(patsubst %.js,$(JSUGLYDIR)/%.min.js,$1)
+
+javascript>capture-to-target = @echo "$1" && $1 > $@ || (cat $@ && exit 1)
+
+UGLIFY.js = $(UGLIFY) $(UGLIFYFLAGS)
+LINT.js = $(JSHINT) $(JSHINTFLAGS)
+TEST.js = $(JSTESTENV) $(JSTEST) $(JSTESTFLAGS)
+
+$(JSUGLYDIR)/%.min.js: %.js | $(UGLIFY)
+       mkdir -p $(@D)
+       $(UGLIFY.js) < $< > $@
+
+$(JSHINTDIR)/%.js.lint: %.js | $(JSHINT)
+       mkdir -p $(@D)
+       $(LINT.js) $<
+       touch $@
+
+$(JSTESTDIR)/%.js.test: %.js | $(JSTEST)
+       mkdir -p $(@D)
+       $(call javascript>capture-to-target,$(TEST.js) $<)
+
+$(npmbindir)/%:
+       $(NPM) install $(firstword $(value $(@F)_npm_package) $(@F))
diff --git a/rules/node-webkit.mk b/rules/node-webkit.mk
new file mode 100644 (file)
index 0000000..8aa0d0b
--- /dev/null
@@ -0,0 +1,91 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+.DELETE_ON_ERROR:
+
+include $(dir $(realpath $(lastword $(MAKEFILE_LIST))))programs.mk
+
+node-webkit-platforms := \
+       osx-x64.zip osx-ia32.zip \
+       linux-x64.tar.gz linux-ia32.tar.gz \
+       win-ia32.zip
+
+node-webkit-pattern := \
+       $(addprefix node-webkit-v%-,$(node-webkit-platforms))
+
+# These are like 30MB, don't download them every time.
+.PRECIOUS: $(node-webkit-pattern)
+
+$(node-webkit-pattern):
+       mkdir -p $(@D)
+       wget -O $@ http://dl.node-webkit.org/v$(*F)/$(@F) || ($(RM) $@ && exit 1)
+
+node-webkit-version ?= 0.10.2
+node-webkit-prefix ?= node-webkit/
+
+node-webkit = $(node-webkit-prefix)node-webkit-v$(firstword $(value node-webkit-version-$1) $(node-webkit-version))-$1
+
+node-webkit-archives = \
+       $(foreach p,$(node-webkit-platforms),$(call node-webkit,$(p)))
+
+define node-webkit-package-osx
+       $(RM) $@
+       $(RM) -r $(@:.zip=)
+       $(UNZIP) -d $(@D) $2
+       mv $(@D)/$(notdir $(2:.zip=)) $(@:.zip=)
+       tools/generate-osx-app $(@:.zip=) $1
+       $(RM) $(@:.zip=)/nwsnapshot
+       mv $(@:.zip=)/credits.html $(@:.zip=)/node-webkit\ credits.html
+       cd $(@D) && $(ZIP) -r $(@F) $(@F:.zip=)
+       $(RM) -r $(@:.zip=)
+endef
+
+%-osx-ia32.zip: %.nw $(call node-webkit,osx-ia32.zip)
+       $(call node-webkit-package-osx,$<,$(word 2,$^))
+
+%-osx-x64.zip: %.nw $(call node-webkit,osx-x64.zip)
+       $(call node-webkit-package-osx,$<,$(word 2,$^))
+
+define node-webkit-package-linux
+       $(RM) $@
+       $(RM) -r $(@:.tar.gz=)
+       tar -C $(@D) -xzf $2
+       mkdir -p $(@:.tar.gz=)
+       mv $(@D)/$(notdir $(2:.tar.gz=)) $(@:.tar.gz=)/nw
+       cp -a $1 $(@:.tar.gz=)/nw/package.nw
+       cp -a tools/nw-linux-wrapper $(@:.tar.gz=)/`echo $(notdir $1) | sed -E 's/-[^-]+$$//'`
+       $(RM) $(@:.tar.gz=)/nw/nwsnapshot
+       mv $(@:.tar.gz=)/nw/credits.html $(@:.tar.gz=)/nw/node-webkit\ credits.html
+       tar -czf $@ -C $(@D) $(@F:.tar.gz=)
+       $(RM) -r $(@:.tar.gz=)
+endef
+
+%-linux-ia32.tar.gz: %.nw $(call node-webkit,linux-ia32.tar.gz)
+       $(call node-webkit-package-linux,$<,$(word 2,$^))
+
+%-linux-x64.tar.gz: %.nw $(call node-webkit,linux-x64.tar.gz)
+       $(call node-webkit-package-linux,$<,$(word 2,$^))
+
+WINE ?= wine
+
+node-webkit-icon = $(shell $(UNZIP) -p $1 package.json | grep -Eo '"[^"]+.ico"' -m 1)
+
+define node-webkit-package-win
+       $(RM) $@
+       $(RM) -r $(@:.zip=)
+       if $(UNZIP) -l $2 credits.html > /dev/null; then $(UNZIP) -d $(@D)/$(notdir $(2:.zip=)) $2; else $(UNZIP) -d $(@D) $2; fi
+       mv $(@D)/$(notdir $(2:.zip=)) $(@:.zip=)
+       $(RM) $(@:.zip=)/nwsnapshot.exe
+       $(UNZIP) -p $< $(call node-webkit-icon,$<) > $(@D)/icon.ico
+       $(WINE) tools/rcedit.exe $(@:.zip=)/nw.exe --set-icon $(@D)/icon.ico
+       $(RM) $(@D)/icon.ico
+       mv $(@:.zip=)/credits.html $(@:.zip=)/node-webkit\ credits.html
+       cp -a $< $(@:.zip=)/package.nw
+       mv $(@:.zip=)/nw.exe $(@:.zip=)/`echo $(notdir $1) | sed -E 's/-[^-]+$$/.exe/'`
+       cd $(@D) && $(ZIP) -r $(@F) $(@F:.zip=)
+       $(RM) -r $(@:.zip=)
+endef
+
+%-win-ia32.zip: %.nw $(call node-webkit,win-ia32.zip)
+       $(call node-webkit-package-win,$<,$(word 2,$^))
diff --git a/rules/pngcrush.mk b/rules/pngcrush.mk
new file mode 100644 (file)
index 0000000..39630df
--- /dev/null
@@ -0,0 +1,20 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+.DELETE_ON_ERROR:
+
+BUILDDIR ?= build/
+PNGCRUSHSTAMPDIR ?= $(BUILDDIR)/stamp
+
+pngcrush-stampify = $(patsubst %.png,$(PNGCRUSHSTAMPDIR)/%.png.crushed,$1)
+
+PNGCRUSH ?= pngcrush
+PNGCRUSHFLAGS ?= -brute -blacken -reduce -q
+
+CRUSH.png ?= $(PNGCRUSH) $(PNGCRUSHFLAGS)
+
+$(PNGCRUSHSTAMPDIR)/%.png.crushed: %.png
+       $(CRUSH.png) -ow $<
+       mkdir -p $(@D)
+       touch $@
diff --git a/rules/programs.mk b/rules/programs.mk
new file mode 100644 (file)
index 0000000..d706d3f
--- /dev/null
@@ -0,0 +1,13 @@
+# This is free and unencumbered software released into the public
+# domain. To the extent possible under law, the author of this file
+# waives all copyright and related or neighboring rights to it.
+
+ZIPFLAGS ?= -q
+UNZIPFLAGS ?= -q
+
+UNZIP = unzip $(UNZIPFLAGS)
+ZIP = zip $(ZIPFLAGS)
+
+ifneq ($(OS),Windows_NT)
+       WINE ?= wine
+endif
diff --git a/src/data/images/.gitignore b/src/data/images/.gitignore
new file mode 100644 (file)
index 0000000..206018c
--- /dev/null
@@ -0,0 +1,5 @@
+icons.ico
+icons.icns
+book.png
+circle.png
+
diff --git a/src/data/images/book.xcf.gz b/src/data/images/book.xcf.gz
new file mode 100644 (file)
index 0000000..cc06d1f
Binary files /dev/null and b/src/data/images/book.xcf.gz differ
diff --git a/src/data/images/circle-inner.png b/src/data/images/circle-inner.png
new file mode 100644 (file)
index 0000000..9069a43
Binary files /dev/null and b/src/data/images/circle-inner.png differ
diff --git a/src/data/images/circle-outer-ee.png b/src/data/images/circle-outer-ee.png
new file mode 100644 (file)
index 0000000..7040514
Binary files /dev/null and b/src/data/images/circle-outer-ee.png differ
diff --git a/src/data/images/circle-outer.png b/src/data/images/circle-outer.png
new file mode 100644 (file)
index 0000000..0c2e236
Binary files /dev/null and b/src/data/images/circle-outer.png differ
diff --git a/src/data/images/circle-rim.png b/src/data/images/circle-rim.png
new file mode 100644 (file)
index 0000000..366d020
Binary files /dev/null and b/src/data/images/circle-rim.png differ
diff --git a/src/data/images/circle.xcf.gz b/src/data/images/circle.xcf.gz
new file mode 100644 (file)
index 0000000..4c2fa51
Binary files /dev/null and b/src/data/images/circle.xcf.gz differ
diff --git a/src/data/images/hand.png b/src/data/images/hand.png
new file mode 100644 (file)
index 0000000..96e07b2
Binary files /dev/null and b/src/data/images/hand.png differ
diff --git a/src/data/images/icons.iconset/icon_128x128.png b/src/data/images/icons.iconset/icon_128x128.png
new file mode 100644 (file)
index 0000000..6c93aa7
Binary files /dev/null and b/src/data/images/icons.iconset/icon_128x128.png differ
diff --git a/src/data/images/icons.iconset/icon_128x128@2x.png b/src/data/images/icons.iconset/icon_128x128@2x.png
new file mode 100644 (file)
index 0000000..08bd681
Binary files /dev/null and b/src/data/images/icons.iconset/icon_128x128@2x.png differ
diff --git a/src/data/images/icons.iconset/icon_16x16.png b/src/data/images/icons.iconset/icon_16x16.png
new file mode 100644 (file)
index 0000000..570b181
Binary files /dev/null and b/src/data/images/icons.iconset/icon_16x16.png differ
diff --git a/src/data/images/icons.iconset/icon_256x256.png b/src/data/images/icons.iconset/icon_256x256.png
new file mode 100644 (file)
index 0000000..08bd681
Binary files /dev/null and b/src/data/images/icons.iconset/icon_256x256.png differ
diff --git a/src/data/images/icons.iconset/icon_32x32.png b/src/data/images/icons.iconset/icon_32x32.png
new file mode 100644 (file)
index 0000000..6813ad9
Binary files /dev/null and b/src/data/images/icons.iconset/icon_32x32.png differ
diff --git a/src/data/images/icons.iconset/icon_32x32@2x.png b/src/data/images/icons.iconset/icon_32x32@2x.png
new file mode 100644 (file)
index 0000000..a8dc504
Binary files /dev/null and b/src/data/images/icons.iconset/icon_32x32@2x.png differ
diff --git a/src/data/images/icons.iconset/icon_64x64.png b/src/data/images/icons.iconset/icon_64x64.png
new file mode 100644 (file)
index 0000000..a8dc504
Binary files /dev/null and b/src/data/images/icons.iconset/icon_64x64.png differ
diff --git a/src/data/images/icons.iconset/icon_64x64@2x.png b/src/data/images/icons.iconset/icon_64x64@2x.png
new file mode 100644 (file)
index 0000000..6c93aa7
Binary files /dev/null and b/src/data/images/icons.iconset/icon_64x64@2x.png differ
diff --git a/src/data/images/sigils.png b/src/data/images/sigils.png
new file mode 100644 (file)
index 0000000..c69871a
Binary files /dev/null and b/src/data/images/sigils.png differ
diff --git a/src/data/license.txt b/src/data/license.txt
new file mode 100644 (file)
index 0000000..066eda5
--- /dev/null
@@ -0,0 +1,26 @@
+Description : Array and textureless GLSL 2D/3D/4D simplex 
+              noise functions.
+     Author : Ian McEwan, Ashima Arts.
+ Maintainer : ijm
+    Lastmod : 20110822 (ijm)
+    License : Copyright (C) 2011 Ashima Arts. All rights reserved.
+              Distributed under the MIT License. See LICENSE file.
+              https:github.com/ashima/webgl-noise
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/data/shaders/noise.glsl b/src/data/shaders/noise.glsl
new file mode 100644 (file)
index 0000000..84944be
--- /dev/null
@@ -0,0 +1,180 @@
+//
+// Description : Array and textureless GLSL 2D/3D/4D simplex 
+//               noise functions.
+//      Author : Ian McEwan, Ashima Arts.
+//  Maintainer : ijm
+//     Lastmod : 20110822 (ijm)
+//     License : Copyright (C) 2011 Ashima Arts. All rights reserved.
+//               Distributed under the MIT License. See LICENSE file.
+//               https://github.com/ashima/webgl-noise
+//
+/*
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+precision highp float;
+
+vec2 mod289(vec2 x) {
+  return x - floor(x * (1.0 / 289.0)) * 289.0;
+}
+
+vec3 mod289(vec3 x) {
+  return x - floor(x * (1.0 / 289.0)) * 289.0;
+}
+
+vec4 mod289(vec4 x) {
+  return x - floor(x * (1.0 / 289.0)) * 289.0;
+}
+
+vec3 permute(vec3 x) {
+  return mod289(((x*34.0)+1.0)*x);
+}
+
+vec4 permute(vec4 x) {
+     return mod289(((x*34.0)+1.0)*x);
+}
+
+vec4 taylorInvSqrt(vec4 r)
+{
+  return 1.79284291400159 - 0.85373472095314 * r;
+}
+
+float snoise(vec3 v)
+  { 
+  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;
+  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);
+
+// First corner
+  vec3 i  = floor(v + dot(v, C.yyy) );
+  vec3 x0 =   v - i + dot(i, C.xxx) ;
+
+// Other corners
+  vec3 g = step(x0.yzx, x0.xyz);
+  vec3 l = 1.0 - g;
+  vec3 i1 = min( g.xyz, l.zxy );
+  vec3 i2 = max( g.xyz, l.zxy );
+
+  //   x0 = x0 - 0.0 + 0.0 * C.xxx;
+  //   x1 = x0 - i1  + 1.0 * C.xxx;
+  //   x2 = x0 - i2  + 2.0 * C.xxx;
+  //   x3 = x0 - 1.0 + 3.0 * C.xxx;
+  vec3 x1 = x0 - i1 + C.xxx;
+  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
+  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y
+
+// Permutations
+  i = mod289(i); 
+  vec4 p = permute( permute( permute( 
+             i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
+           + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 
+           + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
+
+// Gradients: 7x7 points over a square, mapped onto an octahedron.
+// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
+  float n_ = 0.142857142857; // 1.0/7.0
+  vec3  ns = n_ * D.wyz - D.xzx;
+
+  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)
+
+  vec4 x_ = floor(j * ns.z);
+  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)
+
+  vec4 x = x_ *ns.x + ns.yyyy;
+  vec4 y = y_ *ns.x + ns.yyyy;
+  vec4 h = 1.0 - abs(x) - abs(y);
+
+  vec4 b0 = vec4( x.xy, y.xy );
+  vec4 b1 = vec4( x.zw, y.zw );
+
+  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
+  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
+  vec4 s0 = floor(b0)*2.0 + 1.0;
+  vec4 s1 = floor(b1)*2.0 + 1.0;
+  vec4 sh = -step(h, vec4(0.0));
+
+  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
+  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
+
+  vec3 p0 = vec3(a0.xy,h.x);
+  vec3 p1 = vec3(a0.zw,h.y);
+  vec3 p2 = vec3(a1.xy,h.z);
+  vec3 p3 = vec3(a1.zw,h.w);
+
+//Normalise gradients
+  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
+  p0 *= norm.x;
+  p1 *= norm.y;
+  p2 *= norm.z;
+  p3 *= norm.w;
+
+// Mix final noise value
+  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
+  m = m * m;
+  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), 
+                                dot(p2,x2), dot(p3,x3) ) );
+  }
+
+float snoise(vec2 v)
+  {
+  const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
+                      0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
+                     -0.577350269189626, // -1.0 + 2.0 * C.x
+                      0.024390243902439); // 1.0 / 41.0
+// First corner
+  vec2 i = floor(v + dot(v, C.yy) );
+  vec2 x0 = v - i + dot(i, C.xx);
+
+// Other corners
+  vec2 i1;
+  //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
+  //i1.y = 1.0 - i1.x;
+  i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+  // x0 = x0 - 0.0 + 0.0 * C.xx ;
+  // x1 = x0 - i1 + 1.0 * C.xx ;
+  // x2 = x0 - 1.0 + 2.0 * C.xx ;
+  vec4 x12 = x0.xyxy + C.xxzz;
+  x12.xy -= i1;
+
+// Permutations
+  i = mod289(i); // Avoid truncation effects in permutation
+  vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
++ i.x + vec3(0.0, i1.x, 1.0 ));
+
+  vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
+  m = m*m ;
+  m = m*m ;
+
+// Gradients: 41 points uniformly over a line, mapped onto a diamond.
+// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
+
+  vec3 x = 2.0 * fract(p * C.www) - 1.0;
+  vec3 h = abs(x) - 0.5;
+  vec3 ox = floor(x + 0.5);
+  vec3 a0 = x - ox;
+
+// Normalise gradients implicitly by scaling m
+// Approximation of: m *= inversesqrt( a0*a0 + h*h );
+  m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
+
+// Compute final noise value at P
+  vec3 g;
+  g.x = a0.x * x0.x + h.x * x0.y;
+  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
+  return 130.0 * dot(m, g);
+}
diff --git a/src/data/shaders/noisyblocks.frag b/src/data/shaders/noisyblocks.frag
new file mode 100644 (file)
index 0000000..be6e051
--- /dev/null
@@ -0,0 +1,23 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+precision highp float;
+
+varying vec2 fTexCoord;
+varying vec4 fColor;
+uniform sampler2D tex;
+
+uniform vec2 resolution;
+uniform float cut;
+uniform float range;
+
+void main(void) {
+    vec2 coord = floor(fTexCoord * resolution);
+    vec3 n = vec3(coord.xy, cut);
+    float p = 1.0 - range * abs(snoise(n));
+    vec3 modulated = fColor.rgb * p;
+    vec4 texColor = texture2D(tex, fTexCoord);
+    gl_FragColor = vec4(modulated * fColor.a, fColor.a) * texColor;
+}
diff --git a/src/data/shaders/noisyquads.vert b/src/data/shaders/noisyquads.vert
new file mode 100644 (file)
index 0000000..8388349
--- /dev/null
@@ -0,0 +1,26 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+precision mediump float;
+
+attribute vec3 position;
+attribute vec2 texCoord;
+attribute vec4 color;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 projection;
+
+uniform float cut;
+uniform float range;
+
+varying vec4 fColor;
+
+void main(void) {
+    gl_Position = projection * view * model * vec4(position, 1.0);
+    vec2 n = vec2(texCoord.x, cut);
+    float p = 1.0 - range * abs(snoise(n));
+    fColor = vec4(p * color.rgb, color.a);
+}
diff --git a/src/data/sound/book-appear.wav b/src/data/sound/book-appear.wav
new file mode 100644 (file)
index 0000000..bc86223
Binary files /dev/null and b/src/data/sound/book-appear.wav differ
diff --git a/src/data/sound/book-dismiss.wav b/src/data/sound/book-dismiss.wav
new file mode 100644 (file)
index 0000000..052a8d2
Binary files /dev/null and b/src/data/sound/book-dismiss.wav differ
diff --git a/src/data/sound/click-1.wav b/src/data/sound/click-1.wav
new file mode 100644 (file)
index 0000000..fdeff06
Binary files /dev/null and b/src/data/sound/click-1.wav differ
diff --git a/src/data/sound/click-2.wav b/src/data/sound/click-2.wav
new file mode 100644 (file)
index 0000000..528ebfc
Binary files /dev/null and b/src/data/sound/click-2.wav differ
diff --git a/src/data/sound/clicking.wav b/src/data/sound/clicking.wav
new file mode 100644 (file)
index 0000000..3b99320
Binary files /dev/null and b/src/data/sound/clicking.wav differ
diff --git a/src/data/sound/page-turn-1.wav b/src/data/sound/page-turn-1.wav
new file mode 100644 (file)
index 0000000..8fd4096
Binary files /dev/null and b/src/data/sound/page-turn-1.wav differ
diff --git a/src/data/sound/page-turn-2.wav b/src/data/sound/page-turn-2.wav
new file mode 100644 (file)
index 0000000..cbcedd7
Binary files /dev/null and b/src/data/sound/page-turn-2.wav differ
diff --git a/src/data/sound/page-turn-3.wav b/src/data/sound/page-turn-3.wav
new file mode 100644 (file)
index 0000000..ab8e673
Binary files /dev/null and b/src/data/sound/page-turn-3.wav differ
diff --git a/src/data/sound/regear.wav b/src/data/sound/regear.wav
new file mode 100644 (file)
index 0000000..07d970c
Binary files /dev/null and b/src/data/sound/regear.wav differ
diff --git a/src/data/sound/slam.wav b/src/data/sound/slam.wav
new file mode 100644 (file)
index 0000000..798d6e8
Binary files /dev/null and b/src/data/sound/slam.wav differ
diff --git a/src/data/sound/switch.wav b/src/data/sound/switch.wav
new file mode 100644 (file)
index 0000000..1d657dd
Binary files /dev/null and b/src/data/sound/switch.wav differ
diff --git a/src/data/sound/tick.wav b/src/data/sound/tick.wav
new file mode 100644 (file)
index 0000000..4d1c442
Binary files /dev/null and b/src/data/sound/tick.wav differ
diff --git a/src/data/sound/tock.wav b/src/data/sound/tock.wav
new file mode 100644 (file)
index 0000000..8d4d616
Binary files /dev/null and b/src/data/sound/tock.wav differ
diff --git a/src/data/sound/winding.wav b/src/data/sound/winding.wav
new file mode 100644 (file)
index 0000000..8ecfa4a
Binary files /dev/null and b/src/data/sound/winding.wav differ
diff --git a/src/ext/FiraMono-Bold.woff b/src/ext/FiraMono-Bold.woff
new file mode 100644 (file)
index 0000000..0155c48
Binary files /dev/null and b/src/ext/FiraMono-Bold.woff differ
diff --git a/src/ext/FiraMono-Regular.woff b/src/ext/FiraMono-Regular.woff
new file mode 100644 (file)
index 0000000..29db168
Binary files /dev/null and b/src/ext/FiraMono-Regular.woff differ
diff --git a/src/ext/FiraSans-Bold.woff b/src/ext/FiraSans-Bold.woff
new file mode 100644 (file)
index 0000000..404f7cf
Binary files /dev/null and b/src/ext/FiraSans-Bold.woff differ
diff --git a/src/ext/FiraSans-BoldItalic.woff b/src/ext/FiraSans-BoldItalic.woff
new file mode 100644 (file)
index 0000000..d919d01
Binary files /dev/null and b/src/ext/FiraSans-BoldItalic.woff differ
diff --git a/src/ext/FiraSans-Italic.woff b/src/ext/FiraSans-Italic.woff
new file mode 100644 (file)
index 0000000..1ef173c
Binary files /dev/null and b/src/ext/FiraSans-Italic.woff differ
diff --git a/src/ext/FiraSans-Regular.woff b/src/ext/FiraSans-Regular.woff
new file mode 100644 (file)
index 0000000..7fc569b
Binary files /dev/null and b/src/ext/FiraSans-Regular.woff differ
diff --git a/src/ext/FiraSans-UltraLight.woff b/src/ext/FiraSans-UltraLight.woff
new file mode 100644 (file)
index 0000000..0d00757
Binary files /dev/null and b/src/ext/FiraSans-UltraLight.woff differ
diff --git a/src/ext/FiraSans-UltraLightItalic.woff b/src/ext/FiraSans-UltraLightItalic.woff
new file mode 100644 (file)
index 0000000..5a001cb
Binary files /dev/null and b/src/ext/FiraSans-UltraLightItalic.woff differ
diff --git a/src/ext/font-awesome.woff b/src/ext/font-awesome.woff
new file mode 100644 (file)
index 0000000..9eaecb3
Binary files /dev/null and b/src/ext/font-awesome.woff differ
diff --git a/src/ext/gamepad.js b/src/ext/gamepad.js
new file mode 100644 (file)
index 0000000..62e8856
--- /dev/null
@@ -0,0 +1,172 @@
+(function () {
+    "use strict";
+    /** DOM Event shim for Web Gamepad API
+        https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html
+
+        This adds three new custom events to the window object:
+
+        yuugamepadbuttondown, yuugamepadbuttonup
+            Dispatched when a button on a gamepad is pressed
+            or released.
+        
+            .detail.gamepad - the Gamepad instance with the button
+            .detail.button - the numeric index of the button
+            .detail.mapped - true if the button is known to fit the
+                             "standard" mapping, even if gamepad.mapping
+                             says otherwise
+
+        yuugamepadaxismove
+            Dispatched when an axis on a gamepad changes.
+
+            .detail.gamepad - the Gamepad instance with the axis
+            .detail.axis - the numeric index of the axis
+            .detail.value - the value of the axis
+            .detail.mapped - true if the axis is known to fit the
+                             "standard" mapping, even if gamepad.mapping
+                             says otherwise
+
+        It also polyfills navigator.getGamepads.
+
+        Including this file (e.g. in a <script> tag) is all that's
+        necessary to enable these.
+
+        Because the underlying API is poll-based, events will be
+        dispatched at most at the animation refresh rate. Inputs that
+        exist for less than this time may be dropped.
+     */
+
+    function empty () { return []; }
+
+    if (!navigator.getGamepads)
+        navigator.getGamepads = (
+            navigator.webkitGetGamepads
+            || navigator.mozGetGamepads
+            || empty);
+    if (navigator.getGamepads === empty)
+        return;
+
+    var requestFrame = window.requestAnimationFrame
+        || window.mozRequestAnimationFrame
+        || window.webkitRequestAnimationFrame;
+
+    var PLATFORM = (navigator.platform || "Unknown") + ".";
+
+    var MAPPINGS = {
+        // Maps the logical controller button/axis (the index) to the
+        // equivalent standard controller button/axis (the value),
+        // based on ID.
+        //
+        // This needs to be keyed to the underlying platform as well
+        // because mapping varies based on the USB stack in question.
+        // In theory this can go as far as multiple drivers for one OS
+        // with different mappings, but most platforms / devices have
+        // one canonical driver.
+        'MacIntel.54c-268-PLAYSTATION(R)3 Controller': {
+            buttons: [8, 10, 11, 9, 12, 15, 13, 14, 6, 7,
+                      4, 5, 3, 1, 0, 2, 16],
+            axes: [0, 1, 2, 3]
+        }
+    };
+
+    var states = {};
+
+    function isPressed (button) {
+        return isFinite(button) ? button > 0.1 : button.pressed;
+    }
+
+    function isAppropriateState (state, gamepad) {
+        return state
+            && state.id === gamepad.id
+            && state.buttons.length === gamepad.buttons.length
+            && state.axes.length === gamepad.axes.length;
+    }
+
+    function makeState (gamepad) {
+        var buttons = [];
+        for (var i = 0; i < gamepad.buttons.length; ++i)
+            buttons.push(isPressed(gamepad.buttons[i]));
+        return {
+            buttons: buttons,
+            axes: gamepad.axes.slice(),
+            id: gamepad.id
+        };
+    }
+
+    function getState (gamepad) {
+        var state = states[gamepad.index];
+        // Browsers that do not support the connected/disconnected
+        // events will silently swap in a new gamepad. Try to detect
+        // that and reset the state.
+        if (!isAppropriateState(state, gamepad))
+            state = states[gamepad.index] = makeState(gamepad);
+        return state;
+    }
+
+    function connected (event) {
+        states[event.gamepad.index] = makeState(event.gamepad);
+    }
+
+    function disconnected (event) {
+        delete states[event.gamepad.index];
+    }
+
+    function buttonEvent (gamepad, button, pressed) {
+        var mapping = MAPPINGS[PLATFORM + gamepad.id];
+        var mapped = gamepad.mapping === "standard" || !!mapping;
+        if (gamepad.mapping !== "standard" && mapping)
+            button = mapping.buttons[button];
+
+        return new CustomEvent(
+            pressed ? 'yuugamepadbuttondown' : 'yuugamepadbuttonup',
+            { detail: { gamepad: gamepad, mapped: mapped, button: button } });
+    }
+
+    function axisEvent (gamepad, axis, value) {
+        var mapping = MAPPINGS[PLATFORM + gamepad.id];
+        var mapped = gamepad.mapping === "standard" || !!mapping;
+        if (gamepad.mapping !== "standard" && mapping)
+            axis = mapping.axes[axis];
+
+        return new CustomEvent(
+            'yuugamepadaxismove',
+            { detail: { gamepad: gamepad, mapped: mapped,
+                        axis: axis, value: value } });
+    }
+
+    function pumpGamepad (gamepad) {
+        var state = getState(gamepad);
+        for (var b = 0; b < gamepad.buttons.length; ++b) {
+            var wasPressed = isPressed(state.buttons[b]);
+            var nowPressed = isPressed(gamepad.buttons[b]);
+            if (wasPressed !== nowPressed) {
+                state.buttons[b] = nowPressed;
+                window.dispatchEvent(buttonEvent(gamepad, b, nowPressed));
+            }
+        }
+        for (var a = 0; a < gamepad.axes.length; ++a) {
+            if (gamepad.axes[a] !== state.axes[a]) {
+                var value = state.axes[a] = gamepad.axes[a];
+                window.dispatchEvent(axisEvent(gamepad, a, value));
+            }
+        }
+    }
+
+    function pump () {
+        var gamepads = navigator.getGamepads();
+        for (var i = 0; i < gamepads.length; ++i) {
+            var gamepad = gamepads[i];
+            if (!gamepad)
+                continue;
+            pumpGamepad(gamepad);
+        }
+    }
+
+    requestFrame.call(window, function _ () {
+        pump();
+        requestFrame.call(window, _);
+    });
+
+    window.addEventListener('gamepadconnected', connected);
+    window.addEventListener('gamepaddisconnected', disconnected);
+
+})();
diff --git a/src/ext/gl-matrix.js b/src/ext/gl-matrix.js
new file mode 100644 (file)
index 0000000..3965a55
--- /dev/null
@@ -0,0 +1,4248 @@
+/**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.2.1
+ */
+
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+
+(function(_global) {
+  "use strict";
+
+  var shim = {};
+  if (typeof(exports) === 'undefined') {
+    if(typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+      shim.exports = {};
+      define(function() {
+        return shim.exports;
+      });
+    } else {
+      // gl-matrix lives in a browser, define its namespaces in global
+      shim.exports = typeof(window) !== 'undefined' ? window : _global;
+    }
+  }
+  else {
+    // gl-matrix lives in commonjs, define its namespaces in exports
+    shim.exports = exports;
+  }
+
+  (function(exports) {
+    /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+
+if(!GLMAT_EPSILON) {
+    var GLMAT_EPSILON = 0.000001;
+}
+
+if(!GLMAT_ARRAY_TYPE) {
+    var GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
+}
+
+if(!GLMAT_RANDOM) {
+    var GLMAT_RANDOM = Math.random;
+}
+
+/**
+ * @class Common utilities
+ * @name glMatrix
+ */
+var glMatrix = {};
+
+/**
+ * Sets the type of array used when creating new vectors and matricies
+ *
+ * @param {Type} type Array type, such as Float32Array or Array
+ */
+glMatrix.setMatrixArrayType = function(type) {
+    GLMAT_ARRAY_TYPE = type;
+}
+
+if(typeof(exports) !== 'undefined') {
+    exports.glMatrix = glMatrix;
+}
+
+var degree = Math.PI / 180;
+
+/**
+* Convert Degree To Radian
+*
+* @param {Number} Angle in Degrees
+*/
+glMatrix.toRadian = function(a){
+     return a * degree;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2 Dimensional Vector
+ * @name vec2
+ */
+
+var vec2 = {};
+
+/**
+ * Creates a new, empty vec2
+ *
+ * @returns {vec2} a new 2D vector
+ */
+vec2.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = 0;
+    out[1] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec2 initialized with values from an existing vector
+ *
+ * @param {vec2} a vector to clone
+ * @returns {vec2} a new 2D vector
+ */
+vec2.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = a[0];
+    out[1] = a[1];
+    return out;
+};
+
+/**
+ * Creates a new vec2 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} a new 2D vector
+ */
+vec2.fromValues = function(x, y) {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = x;
+    out[1] = y;
+    return out;
+};
+
+/**
+ * Copy the values from one vec2 to another
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the source vector
+ * @returns {vec2} out
+ */
+vec2.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    return out;
+};
+
+/**
+ * Set the components of a vec2 to the given values
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} out
+ */
+vec2.set = function(out, x, y) {
+    out[0] = x;
+    out[1] = y;
+    return out;
+};
+
+/**
+ * Adds two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.subtract}
+ * @function
+ */
+vec2.sub = vec2.subtract;
+
+/**
+ * Multiplies two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.multiply}
+ * @function
+ */
+vec2.mul = vec2.multiply;
+
+/**
+ * Divides two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.divide}
+ * @function
+ */
+vec2.div = vec2.divide;
+
+/**
+ * Returns the minimum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    return out;
+};
+
+/**
+ * Scales a vec2 by a scalar number
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec2} out
+ */
+vec2.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    return out;
+};
+
+/**
+ * Adds two vec2's after scaling the second operand by a scalar value
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec2} out
+ */
+vec2.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec2.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1];
+    return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.distance}
+ * @function
+ */
+vec2.dist = vec2.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec2.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1];
+    return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredDistance}
+ * @function
+ */
+vec2.sqrDist = vec2.squaredDistance;
+
+/**
+ * Calculates the length of a vec2
+ *
+ * @param {vec2} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec2.length = function (a) {
+    var x = a[0],
+        y = a[1];
+    return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.length}
+ * @function
+ */
+vec2.len = vec2.length;
+
+/**
+ * Calculates the squared length of a vec2
+ *
+ * @param {vec2} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec2.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1];
+    return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredLength}
+ * @function
+ */
+vec2.sqrLen = vec2.squaredLength;
+
+/**
+ * Negates the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to negate
+ * @returns {vec2} out
+ */
+vec2.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    return out;
+};
+
+/**
+ * Normalize a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to normalize
+ * @returns {vec2} out
+ */
+vec2.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1];
+    var len = x*x + y*y;
+    if (len > 0) {
+        //TODO: evaluate use of glm_invsqrt here?
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec2.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1];
+};
+
+/**
+ * Computes the cross product of two vec2's
+ * Note that the cross product must by definition produce a 3D vector
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec3} out
+ */
+vec2.cross = function(out, a, b) {
+    var z = a[0] * b[1] - a[1] * b[0];
+    out[0] = out[1] = 0;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec2} out
+ */
+vec2.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec2} out
+ */
+vec2.random = function (out, scale) {
+    scale = scale || 1.0;
+    var r = GLMAT_RANDOM() * 2.0 * Math.PI;
+    out[0] = Math.cos(r) * scale;
+    out[1] = Math.sin(r) * scale;
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2 = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[2] * y;
+    out[1] = m[1] * x + m[3] * y;
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2d
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2d} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2d = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[2] * y + m[4];
+    out[1] = m[1] * x + m[3] * y + m[5];
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat3
+ * 3rd vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat3} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat3 = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[3] * y + m[6];
+    out[1] = m[1] * x + m[4] * y + m[7];
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat4
+ * 3rd vector component is implicitly '0'
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat4 = function(out, a, m) {
+    var x = a[0], 
+        y = a[1];
+    out[0] = m[0] * x + m[4] * y + m[12];
+    out[1] = m[1] * x + m[5] * y + m[13];
+    return out;
+};
+
+/**
+ * Perform some operation over an array of vec2s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec2.forEach = (function() {
+    var vec = vec2.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 2;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec2} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec2.str = function (a) {
+    return 'vec2(' + a[0] + ', ' + a[1] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec2 = vec2;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 3 Dimensional Vector
+ * @name vec3
+ */
+
+var vec3 = {};
+
+/**
+ * Creates a new, empty vec3
+ *
+ * @returns {vec3} a new 3D vector
+ */
+vec3.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec3 initialized with values from an existing vector
+ *
+ * @param {vec3} a vector to clone
+ * @returns {vec3} a new 3D vector
+ */
+vec3.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    return out;
+};
+
+/**
+ * Creates a new vec3 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} a new 3D vector
+ */
+vec3.fromValues = function(x, y, z) {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Copy the values from one vec3 to another
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the source vector
+ * @returns {vec3} out
+ */
+vec3.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    return out;
+};
+
+/**
+ * Set the components of a vec3 to the given values
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} out
+ */
+vec3.set = function(out, x, y, z) {
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Adds two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    out[2] = a[2] + b[2];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    out[2] = a[2] - b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.subtract}
+ * @function
+ */
+vec3.sub = vec3.subtract;
+
+/**
+ * Multiplies two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    out[2] = a[2] * b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.multiply}
+ * @function
+ */
+vec3.mul = vec3.multiply;
+
+/**
+ * Divides two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    out[2] = a[2] / b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.divide}
+ * @function
+ */
+vec3.div = vec3.divide;
+
+/**
+ * Returns the minimum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    out[2] = Math.min(a[2], b[2]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    out[2] = Math.max(a[2], b[2]);
+    return out;
+};
+
+/**
+ * Scales a vec3 by a scalar number
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec3} out
+ */
+vec3.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    out[2] = a[2] * b;
+    return out;
+};
+
+/**
+ * Adds two vec3's after scaling the second operand by a scalar value
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec3} out
+ */
+vec3.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    out[2] = a[2] + (b[2] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec3.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2];
+    return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.distance}
+ * @function
+ */
+vec3.dist = vec3.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec3.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2];
+    return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredDistance}
+ * @function
+ */
+vec3.sqrDist = vec3.squaredDistance;
+
+/**
+ * Calculates the length of a vec3
+ *
+ * @param {vec3} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec3.length = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.length}
+ * @function
+ */
+vec3.len = vec3.length;
+
+/**
+ * Calculates the squared length of a vec3
+ *
+ * @param {vec3} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec3.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredLength}
+ * @function
+ */
+vec3.sqrLen = vec3.squaredLength;
+
+/**
+ * Negates the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to negate
+ * @returns {vec3} out
+ */
+vec3.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    return out;
+};
+
+/**
+ * Normalize a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to normalize
+ * @returns {vec3} out
+ */
+vec3.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    var len = x*x + y*y + z*z;
+    if (len > 0) {
+        //TODO: evaluate use of glm_invsqrt here?
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+        out[2] = a[2] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec3.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+};
+
+/**
+ * Computes the cross product of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.cross = function(out, a, b) {
+    var ax = a[0], ay = a[1], az = a[2],
+        bx = b[0], by = b[1], bz = b[2];
+
+    out[0] = ay * bz - az * by;
+    out[1] = az * bx - ax * bz;
+    out[2] = ax * by - ay * bx;
+    return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+vec3.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1],
+        az = a[2];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    out[2] = az + t * (b[2] - az);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec3} out
+ */
+vec3.random = function (out, scale) {
+    scale = scale || 1.0;
+
+    var r = GLMAT_RANDOM() * 2.0 * Math.PI;
+    var z = (GLMAT_RANDOM() * 2.0) - 1.0;
+    var zScale = Math.sqrt(1.0-z*z) * scale;
+
+    out[0] = Math.cos(r) * zScale;
+    out[1] = Math.sin(r) * zScale;
+    out[2] = z * scale;
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a mat4.
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat4 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2];
+    out[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
+    out[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
+    out[2] = m[2] * x + m[6] * y + m[10] * z + m[14];
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a mat3.
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m the 3x3 matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat3 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2];
+    out[0] = x * m[0] + y * m[3] + z * m[6];
+    out[1] = x * m[1] + y * m[4] + z * m[7];
+    out[2] = x * m[2] + y * m[5] + z * m[8];
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a quat
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec3} out
+ */
+vec3.transformQuat = function(out, a, q) {
+    // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations
+
+    var x = a[0], y = a[1], z = a[2],
+        qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+        // calculate quat * vec
+        ix = qw * x + qy * z - qz * y,
+        iy = qw * y + qz * x - qx * z,
+        iz = qw * z + qx * y - qy * x,
+        iw = -qx * x - qy * y - qz * z;
+
+    // calculate result * inverse quat
+    out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+    out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+    out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+    return out;
+};
+
+/*
+* Rotate a 3D vector around the x-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateX = function(out, a, b, c){
+   var p = [], r=[];
+         //Translate point to the origin
+         p[0] = a[0] - b[0];
+         p[1] = a[1] - b[1];
+       p[2] = a[2] - b[2];
+
+         //perform rotation
+         r[0] = p[0];
+         r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c);
+         r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c);
+
+         //translate to correct position
+         out[0] = r[0] + b[0];
+         out[1] = r[1] + b[1];
+         out[2] = r[2] + b[2];
+
+       return out;
+};
+
+/*
+* Rotate a 3D vector around the y-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateY = function(out, a, b, c){
+       var p = [], r=[];
+       //Translate point to the origin
+       p[0] = a[0] - b[0];
+       p[1] = a[1] - b[1];
+       p[2] = a[2] - b[2];
+  
+       //perform rotation
+       r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c);
+       r[1] = p[1];
+       r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c);
+  
+       //translate to correct position
+       out[0] = r[0] + b[0];
+       out[1] = r[1] + b[1];
+       out[2] = r[2] + b[2];
+  
+       return out;
+};
+
+/*
+* Rotate a 3D vector around the z-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateZ = function(out, a, b, c){
+       var p = [], r=[];
+       //Translate point to the origin
+       p[0] = a[0] - b[0];
+       p[1] = a[1] - b[1];
+       p[2] = a[2] - b[2];
+  
+       //perform rotation
+       r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c);
+       r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c);
+       r[2] = p[2];
+  
+       //translate to correct position
+       out[0] = r[0] + b[0];
+       out[1] = r[1] + b[1];
+       out[2] = r[2] + b[2];
+  
+       return out;
+};
+
+/**
+ * Perform some operation over an array of vec3s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec3.forEach = (function() {
+    var vec = vec3.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 3;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec3} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec3.str = function (a) {
+    return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec3 = vec3;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 4 Dimensional Vector
+ * @name vec4
+ */
+
+var vec4 = {};
+
+/**
+ * Creates a new, empty vec4
+ *
+ * @returns {vec4} a new 4D vector
+ */
+vec4.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec4 initialized with values from an existing vector
+ *
+ * @param {vec4} a vector to clone
+ * @returns {vec4} a new 4D vector
+ */
+vec4.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Creates a new vec4 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} a new 4D vector
+ */
+vec4.fromValues = function(x, y, z, w) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = w;
+    return out;
+};
+
+/**
+ * Copy the values from one vec4 to another
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the source vector
+ * @returns {vec4} out
+ */
+vec4.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Set the components of a vec4 to the given values
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} out
+ */
+vec4.set = function(out, x, y, z, w) {
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = w;
+    return out;
+};
+
+/**
+ * Adds two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    out[2] = a[2] + b[2];
+    out[3] = a[3] + b[3];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    out[2] = a[2] - b[2];
+    out[3] = a[3] - b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.subtract}
+ * @function
+ */
+vec4.sub = vec4.subtract;
+
+/**
+ * Multiplies two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    out[2] = a[2] * b[2];
+    out[3] = a[3] * b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.multiply}
+ * @function
+ */
+vec4.mul = vec4.multiply;
+
+/**
+ * Divides two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    out[2] = a[2] / b[2];
+    out[3] = a[3] / b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.divide}
+ * @function
+ */
+vec4.div = vec4.divide;
+
+/**
+ * Returns the minimum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    out[2] = Math.min(a[2], b[2]);
+    out[3] = Math.min(a[3], b[3]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    out[2] = Math.max(a[2], b[2]);
+    out[3] = Math.max(a[3], b[3]);
+    return out;
+};
+
+/**
+ * Scales a vec4 by a scalar number
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec4} out
+ */
+vec4.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    out[2] = a[2] * b;
+    out[3] = a[3] * b;
+    return out;
+};
+
+/**
+ * Adds two vec4's after scaling the second operand by a scalar value
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec4} out
+ */
+vec4.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    out[2] = a[2] + (b[2] * scale);
+    out[3] = a[3] + (b[3] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec4.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2],
+        w = b[3] - a[3];
+    return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.distance}
+ * @function
+ */
+vec4.dist = vec4.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec4.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2],
+        w = b[3] - a[3];
+    return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredDistance}
+ * @function
+ */
+vec4.sqrDist = vec4.squaredDistance;
+
+/**
+ * Calculates the length of a vec4
+ *
+ * @param {vec4} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec4.length = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.length}
+ * @function
+ */
+vec4.len = vec4.length;
+
+/**
+ * Calculates the squared length of a vec4
+ *
+ * @param {vec4} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec4.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredLength}
+ * @function
+ */
+vec4.sqrLen = vec4.squaredLength;
+
+/**
+ * Negates the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to negate
+ * @returns {vec4} out
+ */
+vec4.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] = -a[3];
+    return out;
+};
+
+/**
+ * Normalize a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to normalize
+ * @returns {vec4} out
+ */
+vec4.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    var len = x*x + y*y + z*z + w*w;
+    if (len > 0) {
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+        out[2] = a[2] * len;
+        out[3] = a[3] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec4.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+};
+
+/**
+ * Performs a linear interpolation between two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec4} out
+ */
+vec4.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1],
+        az = a[2],
+        aw = a[3];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    out[2] = az + t * (b[2] - az);
+    out[3] = aw + t * (b[3] - aw);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec4} out
+ */
+vec4.random = function (out, scale) {
+    scale = scale || 1.0;
+
+    //TODO: This is a pretty awful way of doing this. Find something better.
+    out[0] = GLMAT_RANDOM();
+    out[1] = GLMAT_RANDOM();
+    out[2] = GLMAT_RANDOM();
+    out[3] = GLMAT_RANDOM();
+    vec4.normalize(out, out);
+    vec4.scale(out, out, scale);
+    return out;
+};
+
+/**
+ * Transforms the vec4 with a mat4.
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec4} out
+ */
+vec4.transformMat4 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2], w = a[3];
+    out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
+    out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
+    out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
+    out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
+    return out;
+};
+
+/**
+ * Transforms the vec4 with a quat
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec4} out
+ */
+vec4.transformQuat = function(out, a, q) {
+    var x = a[0], y = a[1], z = a[2],
+        qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+        // calculate quat * vec
+        ix = qw * x + qy * z - qz * y,
+        iy = qw * y + qz * x - qx * z,
+        iz = qw * z + qx * y - qy * x,
+        iw = -qx * x - qy * y - qz * z;
+
+    // calculate result * inverse quat
+    out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+    out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+    out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+    return out;
+};
+
+/**
+ * Perform some operation over an array of vec4s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec4.forEach = (function() {
+    var vec = vec4.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 4;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec4} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec4.str = function (a) {
+    return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec4 = vec4;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2x2 Matrix
+ * @name mat2
+ */
+
+var mat2 = {};
+
+/**
+ * Creates a new identity mat2
+ *
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Creates a new mat2 initialized with values from an existing matrix
+ *
+ * @param {mat2} a matrix to clone
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Copy the values from one mat2 to another
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Set a mat2 to the identity matrix
+ *
+ * @param {mat2} out the receiving matrix
+ * @returns {mat2} out
+ */
+mat2.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a1 = a[1];
+        out[1] = a[2];
+        out[2] = a1;
+    } else {
+        out[0] = a[0];
+        out[1] = a[2];
+        out[2] = a[1];
+        out[3] = a[3];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.invert = function(out, a) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+
+        // Calculate the determinant
+        det = a0 * a3 - a2 * a1;
+
+    if (!det) {
+        return null;
+    }
+    det = 1.0 / det;
+    
+    out[0] =  a3 * det;
+    out[1] = -a1 * det;
+    out[2] = -a2 * det;
+    out[3] =  a0 * det;
+
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.adjoint = function(out, a) {
+    // Caching this value is nessecary if out == a
+    var a0 = a[0];
+    out[0] =  a[3];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] =  a0;
+
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat2
+ *
+ * @param {mat2} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2.determinant = function (a) {
+    return a[0] * a[3] - a[2] * a[1];
+};
+
+/**
+ * Multiplies two mat2's
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the first operand
+ * @param {mat2} b the second operand
+ * @returns {mat2} out
+ */
+mat2.multiply = function (out, a, b) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+    var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+    out[0] = a0 * b0 + a2 * b1;
+    out[1] = a1 * b0 + a3 * b1;
+    out[2] = a0 * b2 + a2 * b3;
+    out[3] = a1 * b2 + a3 * b3;
+    return out;
+};
+
+/**
+ * Alias for {@link mat2.multiply}
+ * @function
+ */
+mat2.mul = mat2.multiply;
+
+/**
+ * Rotates a mat2 by the given angle
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+mat2.rotate = function (out, a, rad) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+    out[0] = a0 *  c + a2 * s;
+    out[1] = a1 *  c + a3 * s;
+    out[2] = a0 * -s + a2 * c;
+    out[3] = a1 * -s + a3 * c;
+    return out;
+};
+
+/**
+ * Scales the mat2 by the dimensions in the given vec2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2} out
+ **/
+mat2.scale = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0 * v0;
+    out[1] = a1 * v0;
+    out[2] = a2 * v1;
+    out[3] = a3 * v1;
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat2
+ *
+ * @param {mat2} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2.str = function (a) {
+    return 'mat2(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2
+ *
+ * @param {mat2} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2)))
+};
+
+/**
+ * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix
+ * @param {mat2} L the lower triangular matrix 
+ * @param {mat2} D the diagonal matrix 
+ * @param {mat2} U the upper triangular matrix 
+ * @param {mat2} a the input matrix to factorize
+ */
+
+mat2.LDU = function (L, D, U, a) { 
+    L[2] = a[2]/a[0]; 
+    U[0] = a[0]; 
+    U[1] = a[1]; 
+    U[3] = a[3] - L[2] * U[1]; 
+    return [L, D, U];       
+}; 
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat2 = mat2;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2x3 Matrix
+ * @name mat2d
+ * 
+ * @description 
+ * A mat2d contains six elements defined as:
+ * <pre>
+ * [a, c, tx,
+ *  b, d, ty]
+ * </pre>
+ * This is a short form for the 3x3 matrix:
+ * <pre>
+ * [a, c, tx,
+ *  b, d, ty,
+ *  0, 0, 1]
+ * </pre>
+ * The last row is ignored so the array is shorter and operations are faster.
+ */
+
+var mat2d = {};
+
+/**
+ * Creates a new identity mat2d
+ *
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(6);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    out[4] = 0;
+    out[5] = 0;
+    return out;
+};
+
+/**
+ * Creates a new mat2d initialized with values from an existing matrix
+ *
+ * @param {mat2d} a matrix to clone
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(6);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    return out;
+};
+
+/**
+ * Copy the values from one mat2d to another
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    return out;
+};
+
+/**
+ * Set a mat2d to the identity matrix
+ *
+ * @param {mat2d} out the receiving matrix
+ * @returns {mat2d} out
+ */
+mat2d.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    out[4] = 0;
+    out[5] = 0;
+    return out;
+};
+
+/**
+ * Inverts a mat2d
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.invert = function(out, a) {
+    var aa = a[0], ab = a[1], ac = a[2], ad = a[3],
+        atx = a[4], aty = a[5];
+
+    var det = aa * ad - ab * ac;
+    if(!det){
+        return null;
+    }
+    det = 1.0 / det;
+
+    out[0] = ad * det;
+    out[1] = -ab * det;
+    out[2] = -ac * det;
+    out[3] = aa * det;
+    out[4] = (ac * aty - ad * atx) * det;
+    out[5] = (ab * atx - aa * aty) * det;
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat2d
+ *
+ * @param {mat2d} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2d.determinant = function (a) {
+    return a[0] * a[3] - a[1] * a[2];
+};
+
+/**
+ * Multiplies two mat2d's
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the first operand
+ * @param {mat2d} b the second operand
+ * @returns {mat2d} out
+ */
+mat2d.multiply = function (out, a, b) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
+    out[0] = a0 * b0 + a2 * b1;
+    out[1] = a1 * b0 + a3 * b1;
+    out[2] = a0 * b2 + a2 * b3;
+    out[3] = a1 * b2 + a3 * b3;
+    out[4] = a0 * b4 + a2 * b5 + a4;
+    out[5] = a1 * b4 + a3 * b5 + a5;
+    return out;
+};
+
+/**
+ * Alias for {@link mat2d.multiply}
+ * @function
+ */
+mat2d.mul = mat2d.multiply;
+
+
+/**
+ * Rotates a mat2d by the given angle
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+mat2d.rotate = function (out, a, rad) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+    out[0] = a0 *  c + a2 * s;
+    out[1] = a1 *  c + a3 * s;
+    out[2] = a0 * -s + a2 * c;
+    out[3] = a1 * -s + a3 * c;
+    out[4] = a4;
+    out[5] = a5;
+    return out;
+};
+
+/**
+ * Scales the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.scale = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0 * v0;
+    out[1] = a1 * v0;
+    out[2] = a2 * v1;
+    out[3] = a3 * v1;
+    out[4] = a4;
+    out[5] = a5;
+    return out;
+};
+
+/**
+ * Translates the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to translate the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.translate = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0;
+    out[1] = a1;
+    out[2] = a2;
+    out[3] = a3;
+    out[4] = a0 * v0 + a2 * v1 + a4;
+    out[5] = a1 * v0 + a3 * v1 + a5;
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat2d
+ *
+ * @param {mat2d} a matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2d.str = function (a) {
+    return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + 
+                    a[3] + ', ' + a[4] + ', ' + a[5] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2d
+ *
+ * @param {mat2d} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2d.frob = function (a) { 
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1))
+}; 
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat2d = mat2d;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 3x3 Matrix
+ * @name mat3
+ */
+
+var mat3 = {};
+
+/**
+ * Creates a new identity mat3
+ *
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(9);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 1;
+    out[5] = 0;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 1;
+    return out;
+};
+
+/**
+ * Copies the upper-left 3x3 values into the given mat3.
+ *
+ * @param {mat3} out the receiving 3x3 matrix
+ * @param {mat4} a   the source 4x4 matrix
+ * @returns {mat3} out
+ */
+mat3.fromMat4 = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[4];
+    out[4] = a[5];
+    out[5] = a[6];
+    out[6] = a[8];
+    out[7] = a[9];
+    out[8] = a[10];
+    return out;
+};
+
+/**
+ * Creates a new mat3 initialized with values from an existing matrix
+ *
+ * @param {mat3} a matrix to clone
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(9);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Copy the values from one mat3 to another
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Set a mat3 to the identity matrix
+ *
+ * @param {mat3} out the receiving matrix
+ * @returns {mat3} out
+ */
+mat3.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 1;
+    out[5] = 0;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a01 = a[1], a02 = a[2], a12 = a[5];
+        out[1] = a[3];
+        out[2] = a[6];
+        out[3] = a01;
+        out[5] = a[7];
+        out[6] = a02;
+        out[7] = a12;
+    } else {
+        out[0] = a[0];
+        out[1] = a[3];
+        out[2] = a[6];
+        out[3] = a[1];
+        out[4] = a[4];
+        out[5] = a[7];
+        out[6] = a[2];
+        out[7] = a[5];
+        out[8] = a[8];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.invert = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        b01 = a22 * a11 - a12 * a21,
+        b11 = -a22 * a10 + a12 * a20,
+        b21 = a21 * a10 - a11 * a20,
+
+        // Calculate the determinant
+        det = a00 * b01 + a01 * b11 + a02 * b21;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = b01 * det;
+    out[1] = (-a22 * a01 + a02 * a21) * det;
+    out[2] = (a12 * a01 - a02 * a11) * det;
+    out[3] = b11 * det;
+    out[4] = (a22 * a00 - a02 * a20) * det;
+    out[5] = (-a12 * a00 + a02 * a10) * det;
+    out[6] = b21 * det;
+    out[7] = (-a21 * a00 + a01 * a20) * det;
+    out[8] = (a11 * a00 - a01 * a10) * det;
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.adjoint = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8];
+
+    out[0] = (a11 * a22 - a12 * a21);
+    out[1] = (a02 * a21 - a01 * a22);
+    out[2] = (a01 * a12 - a02 * a11);
+    out[3] = (a12 * a20 - a10 * a22);
+    out[4] = (a00 * a22 - a02 * a20);
+    out[5] = (a02 * a10 - a00 * a12);
+    out[6] = (a10 * a21 - a11 * a20);
+    out[7] = (a01 * a20 - a00 * a21);
+    out[8] = (a00 * a11 - a01 * a10);
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat3
+ *
+ * @param {mat3} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat3.determinant = function (a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8];
+
+    return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20);
+};
+
+/**
+ * Multiplies two mat3's
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the first operand
+ * @param {mat3} b the second operand
+ * @returns {mat3} out
+ */
+mat3.multiply = function (out, a, b) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        b00 = b[0], b01 = b[1], b02 = b[2],
+        b10 = b[3], b11 = b[4], b12 = b[5],
+        b20 = b[6], b21 = b[7], b22 = b[8];
+
+    out[0] = b00 * a00 + b01 * a10 + b02 * a20;
+    out[1] = b00 * a01 + b01 * a11 + b02 * a21;
+    out[2] = b00 * a02 + b01 * a12 + b02 * a22;
+
+    out[3] = b10 * a00 + b11 * a10 + b12 * a20;
+    out[4] = b10 * a01 + b11 * a11 + b12 * a21;
+    out[5] = b10 * a02 + b11 * a12 + b12 * a22;
+
+    out[6] = b20 * a00 + b21 * a10 + b22 * a20;
+    out[7] = b20 * a01 + b21 * a11 + b22 * a21;
+    out[8] = b20 * a02 + b21 * a12 + b22 * a22;
+    return out;
+};
+
+/**
+ * Alias for {@link mat3.multiply}
+ * @function
+ */
+mat3.mul = mat3.multiply;
+
+/**
+ * Translate a mat3 by the given vector
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to translate
+ * @param {vec2} v vector to translate by
+ * @returns {mat3} out
+ */
+mat3.translate = function(out, a, v) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+        x = v[0], y = v[1];
+
+    out[0] = a00;
+    out[1] = a01;
+    out[2] = a02;
+
+    out[3] = a10;
+    out[4] = a11;
+    out[5] = a12;
+
+    out[6] = x * a00 + y * a10 + a20;
+    out[7] = x * a01 + y * a11 + a21;
+    out[8] = x * a02 + y * a12 + a22;
+    return out;
+};
+
+/**
+ * Rotates a mat3 by the given angle
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+mat3.rotate = function (out, a, rad) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+
+    out[0] = c * a00 + s * a10;
+    out[1] = c * a01 + s * a11;
+    out[2] = c * a02 + s * a12;
+
+    out[3] = c * a10 - s * a00;
+    out[4] = c * a11 - s * a01;
+    out[5] = c * a12 - s * a02;
+
+    out[6] = a20;
+    out[7] = a21;
+    out[8] = a22;
+    return out;
+};
+
+/**
+ * Scales the mat3 by the dimensions in the given vec2
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat3} out
+ **/
+mat3.scale = function(out, a, v) {
+    var x = v[0], y = v[1];
+
+    out[0] = x * a[0];
+    out[1] = x * a[1];
+    out[2] = x * a[2];
+
+    out[3] = y * a[3];
+    out[4] = y * a[4];
+    out[5] = y * a[5];
+
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Copies the values from a mat2d into a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat2d} a the matrix to copy
+ * @returns {mat3} out
+ **/
+mat3.fromMat2d = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = 0;
+
+    out[3] = a[2];
+    out[4] = a[3];
+    out[5] = 0;
+
+    out[6] = a[4];
+    out[7] = a[5];
+    out[8] = 1;
+    return out;
+};
+
+/**
+* Calculates a 3x3 matrix from the given quaternion
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {quat} q Quaternion to create matrix from
+*
+* @returns {mat3} out
+*/
+mat3.fromQuat = function (out, q) {
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        yx = y * x2,
+        yy = y * y2,
+        zx = z * x2,
+        zy = z * y2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - yy - zz;
+    out[3] = yx - wz;
+    out[6] = zx + wy;
+
+    out[1] = yx + wz;
+    out[4] = 1 - xx - zz;
+    out[7] = zy - wx;
+
+    out[2] = zx - wy;
+    out[5] = zy + wx;
+    out[8] = 1 - xx - yy;
+
+    return out;
+};
+
+/**
+* Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {mat4} a Mat4 to derive the normal matrix from
+*
+* @returns {mat3} out
+*/
+mat3.normalFromMat4 = function (out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32,
+
+        // Calculate the determinant
+        det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+    out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+    out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+
+    out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+    out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+    out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+
+    out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+    out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+    out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat3
+ *
+ * @param {mat3} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat3.str = function (a) {
+    return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + 
+                    a[3] + ', ' + a[4] + ', ' + a[5] + ', ' + 
+                    a[6] + ', ' + a[7] + ', ' + a[8] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat3
+ *
+ * @param {mat3} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat3.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2)))
+};
+
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat3 = mat3;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 4x4 Matrix
+ * @name mat4
+ */
+
+var mat4 = {};
+
+/**
+ * Creates a new identity mat4
+ *
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(16);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = 1;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 1;
+    out[11] = 0;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Creates a new mat4 initialized with values from an existing matrix
+ *
+ * @param {mat4} a matrix to clone
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(16);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    out[9] = a[9];
+    out[10] = a[10];
+    out[11] = a[11];
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Copy the values from one mat4 to another
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    out[9] = a[9];
+    out[10] = a[10];
+    out[11] = a[11];
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Set a mat4 to the identity matrix
+ *
+ * @param {mat4} out the receiving matrix
+ * @returns {mat4} out
+ */
+mat4.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = 1;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 1;
+    out[11] = 0;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a01 = a[1], a02 = a[2], a03 = a[3],
+            a12 = a[6], a13 = a[7],
+            a23 = a[11];
+
+        out[1] = a[4];
+        out[2] = a[8];
+        out[3] = a[12];
+        out[4] = a01;
+        out[6] = a[9];
+        out[7] = a[13];
+        out[8] = a02;
+        out[9] = a12;
+        out[11] = a[14];
+        out[12] = a03;
+        out[13] = a13;
+        out[14] = a23;
+    } else {
+        out[0] = a[0];
+        out[1] = a[4];
+        out[2] = a[8];
+        out[3] = a[12];
+        out[4] = a[1];
+        out[5] = a[5];
+        out[6] = a[9];
+        out[7] = a[13];
+        out[8] = a[2];
+        out[9] = a[6];
+        out[10] = a[10];
+        out[11] = a[14];
+        out[12] = a[3];
+        out[13] = a[7];
+        out[14] = a[11];
+        out[15] = a[15];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.invert = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32,
+
+        // Calculate the determinant
+        det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+    out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+    out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+    out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
+    out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+    out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+    out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+    out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
+    out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+    out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+    out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+    out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
+    out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
+    out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
+    out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
+    out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
+
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.adjoint = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+    out[0]  =  (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22));
+    out[1]  = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22));
+    out[2]  =  (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12));
+    out[3]  = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12));
+    out[4]  = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22));
+    out[5]  =  (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22));
+    out[6]  = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12));
+    out[7]  =  (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12));
+    out[8]  =  (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21));
+    out[9]  = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21));
+    out[10] =  (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11));
+    out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11));
+    out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21));
+    out[13] =  (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21));
+    out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11));
+    out[15] =  (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11));
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat4
+ *
+ * @param {mat4} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat4.determinant = function (a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32;
+
+    // Calculate the determinant
+    return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+};
+
+/**
+ * Multiplies two mat4's
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the first operand
+ * @param {mat4} b the second operand
+ * @returns {mat4} out
+ */
+mat4.multiply = function (out, a, b) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+    // Cache only the current line of the second matrix
+    var b0  = b[0], b1 = b[1], b2 = b[2], b3 = b[3];  
+    out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
+    out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
+    out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
+    out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+    return out;
+};
+
+/**
+ * Alias for {@link mat4.multiply}
+ * @function
+ */
+mat4.mul = mat4.multiply;
+
+/**
+ * Translate a mat4 by the given vector
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to translate
+ * @param {vec3} v vector to translate by
+ * @returns {mat4} out
+ */
+mat4.translate = function (out, a, v) {
+    var x = v[0], y = v[1], z = v[2],
+        a00, a01, a02, a03,
+        a10, a11, a12, a13,
+        a20, a21, a22, a23;
+
+    if (a === out) {
+        out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
+        out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
+        out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
+        out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
+    } else {
+        a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+        a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+        a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+        out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
+        out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
+        out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;
+
+        out[12] = a00 * x + a10 * y + a20 * z + a[12];
+        out[13] = a01 * x + a11 * y + a21 * z + a[13];
+        out[14] = a02 * x + a12 * y + a22 * z + a[14];
+        out[15] = a03 * x + a13 * y + a23 * z + a[15];
+    }
+
+    return out;
+};
+
+/**
+ * Scales the mat4 by the dimensions in the given vec3
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to scale
+ * @param {vec3} v the vec3 to scale the matrix by
+ * @returns {mat4} out
+ **/
+mat4.scale = function(out, a, v) {
+    var x = v[0], y = v[1], z = v[2];
+
+    out[0] = a[0] * x;
+    out[1] = a[1] * x;
+    out[2] = a[2] * x;
+    out[3] = a[3] * x;
+    out[4] = a[4] * y;
+    out[5] = a[5] * y;
+    out[6] = a[6] * y;
+    out[7] = a[7] * y;
+    out[8] = a[8] * z;
+    out[9] = a[9] * z;
+    out[10] = a[10] * z;
+    out[11] = a[11] * z;
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Rotates a mat4 by the given angle
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+mat4.rotate = function (out, a, rad, axis) {
+    var x = axis[0], y = axis[1], z = axis[2],
+        len = Math.sqrt(x * x + y * y + z * z),
+        s, c, t,
+        a00, a01, a02, a03,
+        a10, a11, a12, a13,
+        a20, a21, a22, a23,
+        b00, b01, b02,
+        b10, b11, b12,
+        b20, b21, b22;
+
+    if (Math.abs(len) < GLMAT_EPSILON) { return null; }
+    
+    len = 1 / len;
+    x *= len;
+    y *= len;
+    z *= len;
+
+    s = Math.sin(rad);
+    c = Math.cos(rad);
+    t = 1 - c;
+
+    a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+    a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+    a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+    // Construct the elements of the rotation matrix
+    b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s;
+    b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s;
+    b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c;
+
+    // Perform rotation-specific matrix multiplication
+    out[0] = a00 * b00 + a10 * b01 + a20 * b02;
+    out[1] = a01 * b00 + a11 * b01 + a21 * b02;
+    out[2] = a02 * b00 + a12 * b01 + a22 * b02;
+    out[3] = a03 * b00 + a13 * b01 + a23 * b02;
+    out[4] = a00 * b10 + a10 * b11 + a20 * b12;
+    out[5] = a01 * b10 + a11 * b11 + a21 * b12;
+    out[6] = a02 * b10 + a12 * b11 + a22 * b12;
+    out[7] = a03 * b10 + a13 * b11 + a23 * b12;
+    out[8] = a00 * b20 + a10 * b21 + a20 * b22;
+    out[9] = a01 * b20 + a11 * b21 + a21 * b22;
+    out[10] = a02 * b20 + a12 * b21 + a22 * b22;
+    out[11] = a03 * b20 + a13 * b21 + a23 * b22;
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged last row
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the X axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateX = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a10 = a[4],
+        a11 = a[5],
+        a12 = a[6],
+        a13 = a[7],
+        a20 = a[8],
+        a21 = a[9],
+        a22 = a[10],
+        a23 = a[11];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged rows
+        out[0]  = a[0];
+        out[1]  = a[1];
+        out[2]  = a[2];
+        out[3]  = a[3];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[4] = a10 * c + a20 * s;
+    out[5] = a11 * c + a21 * s;
+    out[6] = a12 * c + a22 * s;
+    out[7] = a13 * c + a23 * s;
+    out[8] = a20 * c - a10 * s;
+    out[9] = a21 * c - a11 * s;
+    out[10] = a22 * c - a12 * s;
+    out[11] = a23 * c - a13 * s;
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Y axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateY = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a00 = a[0],
+        a01 = a[1],
+        a02 = a[2],
+        a03 = a[3],
+        a20 = a[8],
+        a21 = a[9],
+        a22 = a[10],
+        a23 = a[11];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged rows
+        out[4]  = a[4];
+        out[5]  = a[5];
+        out[6]  = a[6];
+        out[7]  = a[7];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[0] = a00 * c - a20 * s;
+    out[1] = a01 * c - a21 * s;
+    out[2] = a02 * c - a22 * s;
+    out[3] = a03 * c - a23 * s;
+    out[8] = a00 * s + a20 * c;
+    out[9] = a01 * s + a21 * c;
+    out[10] = a02 * s + a22 * c;
+    out[11] = a03 * s + a23 * c;
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Z axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateZ = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a00 = a[0],
+        a01 = a[1],
+        a02 = a[2],
+        a03 = a[3],
+        a10 = a[4],
+        a11 = a[5],
+        a12 = a[6],
+        a13 = a[7];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged last row
+        out[8]  = a[8];
+        out[9]  = a[9];
+        out[10] = a[10];
+        out[11] = a[11];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[0] = a00 * c + a10 * s;
+    out[1] = a01 * c + a11 * s;
+    out[2] = a02 * c + a12 * s;
+    out[3] = a03 * c + a13 * s;
+    out[4] = a10 * c - a00 * s;
+    out[5] = a11 * c - a01 * s;
+    out[6] = a12 * c - a02 * s;
+    out[7] = a13 * c - a03 * s;
+    return out;
+};
+
+/**
+ * Creates a matrix from a quaternion rotation and vector translation
+ * This is equivalent to (but much faster than):
+ *
+ *     mat4.identity(dest);
+ *     mat4.translate(dest, vec);
+ *     var quatMat = mat4.create();
+ *     quat4.toMat4(quat, quatMat);
+ *     mat4.multiply(dest, quatMat);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+mat4.fromRotationTranslation = function (out, q, v) {
+    // Quaternion math
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        xy = x * y2,
+        xz = x * z2,
+        yy = y * y2,
+        yz = y * z2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - (yy + zz);
+    out[1] = xy + wz;
+    out[2] = xz - wy;
+    out[3] = 0;
+    out[4] = xy - wz;
+    out[5] = 1 - (xx + zz);
+    out[6] = yz + wx;
+    out[7] = 0;
+    out[8] = xz + wy;
+    out[9] = yz - wx;
+    out[10] = 1 - (xx + yy);
+    out[11] = 0;
+    out[12] = v[0];
+    out[13] = v[1];
+    out[14] = v[2];
+    out[15] = 1;
+    
+    return out;
+};
+
+mat4.fromQuat = function (out, q) {
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        yx = y * x2,
+        yy = y * y2,
+        zx = z * x2,
+        zy = z * y2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - yy - zz;
+    out[1] = yx + wz;
+    out[2] = zx - wy;
+    out[3] = 0;
+
+    out[4] = yx - wz;
+    out[5] = 1 - xx - zz;
+    out[6] = zy + wx;
+    out[7] = 0;
+
+    out[8] = zx + wy;
+    out[9] = zy - wx;
+    out[10] = 1 - xx - yy;
+    out[11] = 0;
+
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+
+    return out;
+};
+
+/**
+ * Generates a frustum matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {Number} left Left bound of the frustum
+ * @param {Number} right Right bound of the frustum
+ * @param {Number} bottom Bottom bound of the frustum
+ * @param {Number} top Top bound of the frustum
+ * @param {Number} near Near bound of the frustum
+ * @param {Number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.frustum = function (out, left, right, bottom, top, near, far) {
+    var rl = 1 / (right - left),
+        tb = 1 / (top - bottom),
+        nf = 1 / (near - far);
+    out[0] = (near * 2) * rl;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = (near * 2) * tb;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = (right + left) * rl;
+    out[9] = (top + bottom) * tb;
+    out[10] = (far + near) * nf;
+    out[11] = -1;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = (far * near * 2) * nf;
+    out[15] = 0;
+    return out;
+};
+
+/**
+ * Generates a perspective projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fovy Vertical field of view in radians
+ * @param {number} aspect Aspect ratio. typically viewport width/height
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.perspective = function (out, fovy, aspect, near, far) {
+    var f = 1.0 / Math.tan(fovy / 2),
+        nf = 1 / (near - far);
+    out[0] = f / aspect;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = f;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = (far + near) * nf;
+    out[11] = -1;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = (2 * far * near) * nf;
+    out[15] = 0;
+    return out;
+};
+
+/**
+ * Generates a orthogonal projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} left Left bound of the frustum
+ * @param {number} right Right bound of the frustum
+ * @param {number} bottom Bottom bound of the frustum
+ * @param {number} top Top bound of the frustum
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.ortho = function (out, left, right, bottom, top, near, far) {
+    var lr = 1 / (left - right),
+        bt = 1 / (bottom - top),
+        nf = 1 / (near - far);
+    out[0] = -2 * lr;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = -2 * bt;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 2 * nf;
+    out[11] = 0;
+    out[12] = (left + right) * lr;
+    out[13] = (top + bottom) * bt;
+    out[14] = (far + near) * nf;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Generates a look-at matrix with the given eye position, focal point, and up axis
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {vec3} eye Position of the viewer
+ * @param {vec3} center Point the viewer is looking at
+ * @param {vec3} up vec3 pointing up
+ * @returns {mat4} out
+ */
+mat4.lookAt = function (out, eye, center, up) {
+    var x0, x1, x2, y0, y1, y2, z0, z1, z2, len,
+        eyex = eye[0],
+        eyey = eye[1],
+        eyez = eye[2],
+        upx = up[0],
+        upy = up[1],
+        upz = up[2],
+        centerx = center[0],
+        centery = center[1],
+        centerz = center[2];
+
+    if (Math.abs(eyex - centerx) < GLMAT_EPSILON &&
+        Math.abs(eyey - centery) < GLMAT_EPSILON &&
+        Math.abs(eyez - centerz) < GLMAT_EPSILON) {
+        return mat4.identity(out);
+    }
+
+    z0 = eyex - centerx;
+    z1 = eyey - centery;
+    z2 = eyez - centerz;
+
+    len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+    z0 *= len;
+    z1 *= len;
+    z2 *= len;
+
+    x0 = upy * z2 - upz * z1;
+    x1 = upz * z0 - upx * z2;
+    x2 = upx * z1 - upy * z0;
+    len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
+    if (!len) {
+        x0 = 0;
+        x1 = 0;
+        x2 = 0;
+    } else {
+        len = 1 / len;
+        x0 *= len;
+        x1 *= len;
+        x2 *= len;
+    }
+
+    y0 = z1 * x2 - z2 * x1;
+    y1 = z2 * x0 - z0 * x2;
+    y2 = z0 * x1 - z1 * x0;
+
+    len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
+    if (!len) {
+        y0 = 0;
+        y1 = 0;
+        y2 = 0;
+    } else {
+        len = 1 / len;
+        y0 *= len;
+        y1 *= len;
+        y2 *= len;
+    }
+
+    out[0] = x0;
+    out[1] = y0;
+    out[2] = z0;
+    out[3] = 0;
+    out[4] = x1;
+    out[5] = y1;
+    out[6] = z1;
+    out[7] = 0;
+    out[8] = x2;
+    out[9] = y2;
+    out[10] = z2;
+    out[11] = 0;
+    out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+    out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+    out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+    out[15] = 1;
+
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat4
+ *
+ * @param {mat4} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat4.str = function (a) {
+    return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' +
+                    a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' +
+                    a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' + 
+                    a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat4
+ *
+ * @param {mat4} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat4.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) ))
+};
+
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat4 = mat4;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class Quaternion
+ * @name quat
+ */
+
+var quat = {};
+
+/**
+ * Creates a new identity quat
+ *
+ * @returns {quat} a new quaternion
+ */
+quat.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Sets a quaternion to represent the shortest rotation from one
+ * vector to another.
+ *
+ * Both vectors are assumed to be unit length.
+ *
+ * @param {quat} out the receiving quaternion.
+ * @param {vec3} a the initial vector
+ * @param {vec3} b the destination vector
+ * @returns {quat} out
+ */
+quat.rotationTo = (function() {
+    var tmpvec3 = vec3.create();
+    var xUnitVec3 = vec3.fromValues(1,0,0);
+    var yUnitVec3 = vec3.fromValues(0,1,0);
+
+    return function(out, a, b) {
+        var dot = vec3.dot(a, b);
+        if (dot < -0.999999) {
+            vec3.cross(tmpvec3, xUnitVec3, a);
+            if (vec3.length(tmpvec3) < 0.000001)
+                vec3.cross(tmpvec3, yUnitVec3, a);
+            vec3.normalize(tmpvec3, tmpvec3);
+            quat.setAxisAngle(out, tmpvec3, Math.PI);
+            return out;
+        } else if (dot > 0.999999) {
+            out[0] = 0;
+            out[1] = 0;
+            out[2] = 0;
+            out[3] = 1;
+            return out;
+        } else {
+            vec3.cross(tmpvec3, a, b);
+            out[0] = tmpvec3[0];
+            out[1] = tmpvec3[1];
+            out[2] = tmpvec3[2];
+            out[3] = 1 + dot;
+            return quat.normalize(out, out);
+        }
+    };
+})();
+
+/**
+ * Sets the specified quaternion with values corresponding to the given
+ * axes. Each axis is a vec3 and is expected to be unit length and
+ * perpendicular to all other specified axes.
+ *
+ * @param {vec3} view  the vector representing the viewing direction
+ * @param {vec3} right the vector representing the local "right" direction
+ * @param {vec3} up    the vector representing the local "up" direction
+ * @returns {quat} out
+ */
+quat.setAxes = (function() {
+    var matr = mat3.create();
+
+    return function(out, view, right, up) {
+        matr[0] = right[0];
+        matr[3] = right[1];
+        matr[6] = right[2];
+
+        matr[1] = up[0];
+        matr[4] = up[1];
+        matr[7] = up[2];
+
+        matr[2] = -view[0];
+        matr[5] = -view[1];
+        matr[8] = -view[2];
+
+        return quat.normalize(out, quat.fromMat3(out, matr));
+    };
+})();
+
+/**
+ * Creates a new quat initialized with values from an existing quaternion
+ *
+ * @param {quat} a quaternion to clone
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.clone = vec4.clone;
+
+/**
+ * Creates a new quat initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.fromValues = vec4.fromValues;
+
+/**
+ * Copy the values from one quat to another
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the source quaternion
+ * @returns {quat} out
+ * @function
+ */
+quat.copy = vec4.copy;
+
+/**
+ * Set the components of a quat to the given values
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} out
+ * @function
+ */
+quat.set = vec4.set;
+
+/**
+ * Set a quat to the identity quaternion
+ *
+ * @param {quat} out the receiving quaternion
+ * @returns {quat} out
+ */
+quat.identity = function(out) {
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Sets a quat from the given angle and rotation axis,
+ * then returns it.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {vec3} axis the axis around which to rotate
+ * @param {Number} rad the angle in radians
+ * @returns {quat} out
+ **/
+quat.setAxisAngle = function(out, axis, rad) {
+    rad = rad * 0.5;
+    var s = Math.sin(rad);
+    out[0] = s * axis[0];
+    out[1] = s * axis[1];
+    out[2] = s * axis[2];
+    out[3] = Math.cos(rad);
+    return out;
+};
+
+/**
+ * Adds two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ * @function
+ */
+quat.add = vec4.add;
+
+/**
+ * Multiplies two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ */
+quat.multiply = function(out, a, b) {
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+    out[0] = ax * bw + aw * bx + ay * bz - az * by;
+    out[1] = ay * bw + aw * by + az * bx - ax * bz;
+    out[2] = az * bw + aw * bz + ax * by - ay * bx;
+    out[3] = aw * bw - ax * bx - ay * by - az * bz;
+    return out;
+};
+
+/**
+ * Alias for {@link quat.multiply}
+ * @function
+ */
+quat.mul = quat.multiply;
+
+/**
+ * Scales a quat by a scalar number
+ *
+ * @param {quat} out the receiving vector
+ * @param {quat} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {quat} out
+ * @function
+ */
+quat.scale = vec4.scale;
+
+/**
+ * Rotates a quaternion by the given angle about the X axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateX = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw + aw * bx;
+    out[1] = ay * bw + az * bx;
+    out[2] = az * bw - ay * bx;
+    out[3] = aw * bw - ax * bx;
+    return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Y axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateY = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        by = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw - az * by;
+    out[1] = ay * bw + aw * by;
+    out[2] = az * bw + ax * by;
+    out[3] = aw * bw - ay * by;
+    return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Z axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateZ = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bz = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw + ay * bz;
+    out[1] = ay * bw - ax * bz;
+    out[2] = az * bw + aw * bz;
+    out[3] = aw * bw - az * bz;
+    return out;
+};
+
+/**
+ * Calculates the W component of a quat from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate W component of
+ * @returns {quat} out
+ */
+quat.calculateW = function (out, a) {
+    var x = a[0], y = a[1], z = a[2];
+
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = -Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z));
+    return out;
+};
+
+/**
+ * Calculates the dot product of two quat's
+ *
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {Number} dot product of a and b
+ * @function
+ */
+quat.dot = vec4.dot;
+
+/**
+ * Performs a linear interpolation between two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ * @function
+ */
+quat.lerp = vec4.lerp;
+
+/**
+ * Performs a spherical linear interpolation between two quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ */
+quat.slerp = function (out, a, b, t) {
+    // benchmarks:
+    //    http://jsperf.com/quaternion-slerp-implementations
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+    var        omega, cosom, sinom, scale0, scale1;
+
+    // calc cosine
+    cosom = ax * bx + ay * by + az * bz + aw * bw;
+    // adjust signs (if necessary)
+    if ( cosom < 0.0 ) {
+        cosom = -cosom;
+        bx = - bx;
+        by = - by;
+        bz = - bz;
+        bw = - bw;
+    }
+    // calculate coefficients
+    if ( (1.0 - cosom) > 0.000001 ) {
+        // standard case (slerp)
+        omega  = Math.acos(cosom);
+        sinom  = Math.sin(omega);
+        scale0 = Math.sin((1.0 - t) * omega) / sinom;
+        scale1 = Math.sin(t * omega) / sinom;
+    } else {        
+        // "from" and "to" quaternions are very close 
+        //  ... so we can do a linear interpolation
+        scale0 = 1.0 - t;
+        scale1 = t;
+    }
+    // calculate final values
+    out[0] = scale0 * ax + scale1 * bx;
+    out[1] = scale0 * ay + scale1 * by;
+    out[2] = scale0 * az + scale1 * bz;
+    out[3] = scale0 * aw + scale1 * bw;
+    
+    return out;
+};
+
+/**
+ * Calculates the inverse of a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate inverse of
+ * @returns {quat} out
+ */
+quat.invert = function(out, a) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        dot = a0*a0 + a1*a1 + a2*a2 + a3*a3,
+        invDot = dot ? 1.0/dot : 0;
+    
+    // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0
+
+    out[0] = -a0*invDot;
+    out[1] = -a1*invDot;
+    out[2] = -a2*invDot;
+    out[3] = a3*invDot;
+    return out;
+};
+
+/**
+ * Calculates the conjugate of a quat
+ * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate conjugate of
+ * @returns {quat} out
+ */
+quat.conjugate = function (out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Calculates the length of a quat
+ *
+ * @param {quat} a vector to calculate length of
+ * @returns {Number} length of a
+ * @function
+ */
+quat.length = vec4.length;
+
+/**
+ * Alias for {@link quat.length}
+ * @function
+ */
+quat.len = quat.length;
+
+/**
+ * Calculates the squared length of a quat
+ *
+ * @param {quat} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ * @function
+ */
+quat.squaredLength = vec4.squaredLength;
+
+/**
+ * Alias for {@link quat.squaredLength}
+ * @function
+ */
+quat.sqrLen = quat.squaredLength;
+
+/**
+ * Normalize a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quaternion to normalize
+ * @returns {quat} out
+ * @function
+ */
+quat.normalize = vec4.normalize;
+
+/**
+ * Creates a quaternion from the given 3x3 rotation matrix.
+ *
+ * NOTE: The resultant quaternion is not normalized, so you should be sure
+ * to renormalize the quaternion yourself where necessary.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {mat3} m rotation matrix
+ * @returns {quat} out
+ * @function
+ */
+quat.fromMat3 = function(out, m) {
+    // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
+    // article "Quaternion Calculus and Fast Animation".
+    var fTrace = m[0] + m[4] + m[8];
+    var fRoot;
+
+    if ( fTrace > 0.0 ) {
+        // |w| > 1/2, may as well choose w > 1/2
+        fRoot = Math.sqrt(fTrace + 1.0);  // 2w
+        out[3] = 0.5 * fRoot;
+        fRoot = 0.5/fRoot;  // 1/(4w)
+        out[0] = (m[7]-m[5])*fRoot;
+        out[1] = (m[2]-m[6])*fRoot;
+        out[2] = (m[3]-m[1])*fRoot;
+    } else {
+        // |w| <= 1/2
+        var i = 0;
+        if ( m[4] > m[0] )
+          i = 1;
+        if ( m[8] > m[i*3+i] )
+          i = 2;
+        var j = (i+1)%3;
+        var k = (i+2)%3;
+        
+        fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0);
+        out[i] = 0.5 * fRoot;
+        fRoot = 0.5 / fRoot;
+        out[3] = (m[k*3+j] - m[j*3+k]) * fRoot;
+        out[j] = (m[j*3+i] + m[i*3+j]) * fRoot;
+        out[k] = (m[k*3+i] + m[i*3+k]) * fRoot;
+    }
+    
+    return out;
+};
+
+/**
+ * Returns a string representation of a quatenion
+ *
+ * @param {quat} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+quat.str = function (a) {
+    return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.quat = quat;
+}
+;
+
+
+
+
+
+
+
+
+
+
+
+
+
+  })(shim.exports);
+})(this);
diff --git a/src/ext/hammer.js b/src/ext/hammer.js
new file mode 100644 (file)
index 0000000..053ae57
--- /dev/null
@@ -0,0 +1,1545 @@
+/*! Hammer.JS - v1.0.10 - 2014-03-28
+ * http://eightmedia.github.io/hammer.js
+ *
+ * Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
+ * Licensed under the MIT license */
+
+(function(window, undefined) {
+  'use strict';
+
+/**
+ * Hammer
+ * use this to create instances
+ * @param   {HTMLElement}   element
+ * @param   {Object}        options
+ * @returns {Hammer.Instance}
+ * @constructor
+ */
+var Hammer = function(element, options) {
+  return new Hammer.Instance(element, options || {});
+};
+
+Hammer.VERSION = '1.0.10';
+
+// default settings
+Hammer.defaults = {
+  // add styles and attributes to the element to prevent the browser from doing
+  // its native behavior. this doesnt prevent the scrolling, but cancels
+  // the contextmenu, tap highlighting etc
+  // set to false to disable this
+  stop_browser_behavior: {
+    // this also triggers onselectstart=false for IE
+    userSelect       : 'none',
+    // this makes the element blocking in IE10>, you could experiment with the value
+    // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
+    touchAction      : 'none',
+    touchCallout     : 'none',
+    contentZooming   : 'none',
+    userDrag         : 'none',
+    tapHighlightColor: 'rgba(0,0,0,0)'
+  }
+
+  //
+  // more settings are defined per gesture at /gestures
+  //
+};
+
+
+// detect touchevents
+Hammer.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
+Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
+
+// dont use mouseevents on mobile devices
+Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
+Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && window.navigator.userAgent.match(Hammer.MOBILE_REGEX);
+
+// eventtypes per touchevent (start, move, end)
+// are filled by Event.determineEventTypes on setup
+Hammer.EVENT_TYPES = {};
+
+// interval in which Hammer recalculates current velocity in ms
+Hammer.UPDATE_VELOCITY_INTERVAL = 16;
+
+// hammer document where the base events are added at
+Hammer.DOCUMENT = window.document;
+
+// define these also as vars, for better minification
+// direction defines
+var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
+var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
+var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
+var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
+
+// pointer type
+var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
+var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
+var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
+
+// touch event defines
+var EVENT_START = Hammer.EVENT_START = 'start';
+var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
+var EVENT_END = Hammer.EVENT_END = 'end';
+
+
+// plugins and gestures namespaces
+Hammer.plugins = Hammer.plugins || {};
+Hammer.gestures = Hammer.gestures || {};
+
+
+// if the window events are set...
+Hammer.READY = false;
+
+
+/**
+ * setup events to detect gestures on the document
+ */
+function setup() {
+  if(Hammer.READY) {
+    return;
+  }
+
+  // find what eventtypes we add listeners to
+  Event.determineEventTypes();
+
+  // Register all gestures inside Hammer.gestures
+  Utils.each(Hammer.gestures, function(gesture){
+    Detection.register(gesture);
+  });
+
+  // Add touch events on the document
+  Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
+  Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
+
+  // Hammer is ready...!
+  Hammer.READY = true;
+}
+
+var Utils = Hammer.utils = {
+  /**
+   * extend method,
+   * also used for cloning when dest is an empty object
+   * @param   {Object}    dest
+   * @param   {Object}    src
+   * @parm  {Boolean}  merge    do a merge
+   * @returns {Object}    dest
+   */
+  extend: function extend(dest, src, merge) {
+    for(var key in src) {
+      if(dest[key] !== undefined && merge) {
+        continue;
+      }
+      dest[key] = src[key];
+    }
+    return dest;
+  },
+
+
+  /**
+   * for each
+   * @param obj
+   * @param iterator
+   */
+  each: function each(obj, iterator, context) {
+    var i, o;
+    // native forEach on arrays
+    if ('forEach' in obj) {
+      obj.forEach(iterator, context);
+    }
+    // arrays
+    else if(obj.length !== undefined) {
+      for(i=-1; (o=obj[++i]);) {
+        if (iterator.call(context, o, i, obj) === false) {
+          return;
+        }
+      }
+    }
+    // objects
+    else {
+      for(i in obj) {
+        if(obj.hasOwnProperty(i) &&
+            iterator.call(context, obj[i], i, obj) === false) {
+          return;
+        }
+      }
+    }
+  },
+
+
+  /**
+   * find if a string contains the needle
+   * @param   {String}  src
+   * @param   {String}  needle
+   * @returns {Boolean} found
+   */
+  inStr: function inStr(src, needle) {
+    return src.indexOf(needle) > -1;
+  },
+
+
+  /**
+   * find if a node is in the given parent
+   * used for event delegation tricks
+   * @param   {HTMLElement}   node
+   * @param   {HTMLElement}   parent
+   * @returns {boolean}       has_parent
+   */
+  hasParent: function hasParent(node, parent) {
+    while(node) {
+      if(node == parent) {
+        return true;
+      }
+      node = node.parentNode;
+    }
+    return false;
+  },
+
+
+  /**
+   * get the center of all the touches
+   * @param   {Array}     touches
+   * @returns {Object}    center pageXY clientXY
+   */
+  getCenter: function getCenter(touches) {
+    var pageX = []
+      , pageY = []
+      , clientX = []
+      , clientY = []
+      , min = Math.min
+      , max = Math.max;
+
+    // no need to loop when only one touch
+    if(touches.length === 1) {
+      return {
+        pageX: touches[0].pageX,
+        pageY: touches[0].pageY,
+        clientX: touches[0].clientX,
+        clientY: touches[0].clientY
+      };
+    }
+
+    Utils.each(touches, function(touch) {
+      pageX.push(touch.pageX);
+      pageY.push(touch.pageY);
+      clientX.push(touch.clientX);
+      clientY.push(touch.clientY);
+    });
+
+    return {
+      pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
+      pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
+      clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
+      clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
+    };
+  },
+
+
+  /**
+   * calculate the velocity between two points
+   * @param   {Number}    delta_time
+   * @param   {Number}    delta_x
+   * @param   {Number}    delta_y
+   * @returns {Object}    velocity
+   */
+  getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
+    return {
+      x: Math.abs(delta_x / delta_time) || 0,
+      y: Math.abs(delta_y / delta_time) || 0
+    };
+  },
+
+
+  /**
+   * calculate the angle between two coordinates
+   * @param   {Touch}     touch1
+   * @param   {Touch}     touch2
+   * @returns {Number}    angle
+   */
+  getAngle: function getAngle(touch1, touch2) {
+    var x = touch2.clientX - touch1.clientX
+      , y = touch2.clientY - touch1.clientY;
+    return Math.atan2(y, x) * 180 / Math.PI;
+  },
+
+
+  /**
+   * angle to direction define
+   * @param   {Touch}     touch1
+   * @param   {Touch}     touch2
+   * @returns {String}    direction constant, like DIRECTION_LEFT
+   */
+  getDirection: function getDirection(touch1, touch2) {
+    var x = Math.abs(touch1.clientX - touch2.clientX)
+      , y = Math.abs(touch1.clientY - touch2.clientY);
+    if(x >= y) {
+      return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
+    }
+    return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
+  },
+
+
+  /**
+   * calculate the distance between two touches
+   * @param   {Touch}     touch1
+   * @param   {Touch}     touch2
+   * @returns {Number}    distance
+   */
+  getDistance: function getDistance(touch1, touch2) {
+    var x = touch2.clientX - touch1.clientX
+      , y = touch2.clientY - touch1.clientY;
+    return Math.sqrt((x * x) + (y * y));
+  },
+
+
+  /**
+   * calculate the scale factor between two touchLists (fingers)
+   * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
+   * @param   {Array}     start
+   * @param   {Array}     end
+   * @returns {Number}    scale
+   */
+  getScale: function getScale(start, end) {
+    // need two fingers...
+    if(start.length >= 2 && end.length >= 2) {
+      return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
+    }
+    return 1;
+  },
+
+
+  /**
+   * calculate the rotation degrees between two touchLists (fingers)
+   * @param   {Array}     start
+   * @param   {Array}     end
+   * @returns {Number}    rotation
+   */
+  getRotation: function getRotation(start, end) {
+    // need two fingers
+    if(start.length >= 2 && end.length >= 2) {
+      return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
+    }
+    return 0;
+  },
+
+
+  /**
+   * boolean if the direction is vertical
+   * @param    {String}    direction
+   * @returns  {Boolean}   is_vertical
+   */
+  isVertical: function isVertical(direction) {
+    return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
+  },
+
+
+  /**
+   * toggle browser default behavior with css props
+   * @param   {HtmlElement}   element
+   * @param   {Object}        css_props
+   * @param   {Boolean}       toggle
+   */
+  toggleDefaultBehavior: function toggleDefaultBehavior(element, css_props, toggle) {
+    if(!css_props || !element || !element.style) {
+      return;
+    }
+
+    // with css properties for modern browsers
+    Utils.each(['webkit', 'moz', 'Moz', 'ms', 'o', ''], function setStyle(vendor) {
+      Utils.each(css_props, function(value, prop) {
+          // vender prefix at the property
+          if(vendor) {
+            prop = vendor + prop.substring(0, 1).toUpperCase() + prop.substring(1);
+          }
+          // set the style
+          if(prop in element.style) {
+            element.style[prop] = !toggle && value;
+          }
+      });
+    });
+
+    var false_fn = function(){ return false; };
+
+    // also the disable onselectstart
+    if(css_props.userSelect == 'none') {
+      element.onselectstart = !toggle && false_fn;
+    }
+    // and disable ondragstart
+    if(css_props.userDrag == 'none') {
+      element.ondragstart = !toggle && false_fn;
+    }
+  }
+};
+
+
+/**
+ * create new hammer instance
+ * all methods should return the instance itself, so it is chainable.
+ * @param   {HTMLElement}       element
+ * @param   {Object}            [options={}]
+ * @returns {Hammer.Instance}
+ * @constructor
+ */
+Hammer.Instance = function(element, options) {
+  var self = this;
+
+  // setup HammerJS window events and register all gestures
+  // this also sets up the default options
+  setup();
+
+  this.element = element;
+
+  // start/stop detection option
+  this.enabled = true;
+
+  // merge options
+  this.options = Utils.extend(
+    Utils.extend({}, Hammer.defaults),
+    options || {});
+
+  // add some css to the element to prevent the browser from doing its native behavoir
+  if(this.options.stop_browser_behavior) {
+    Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, false);
+  }
+
+  // start detection on touchstart
+  this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
+    if(self.enabled) {
+      Detection.startDetect(self, ev);
+    }
+  });
+
+  // keep a list of user event handlers which needs to be removed when calling 'dispose'
+  this.eventHandlers = [];
+
+  // return instance
+  return this;
+};
+
+
+Hammer.Instance.prototype = {
+  /**
+   * bind events to the instance
+   * @param   {String}      gesture
+   * @param   {Function}    handler
+   * @returns {Hammer.Instance}
+   */
+  on: function onEvent(gesture, handler) {
+    var gestures = gesture.split(' ');
+    Utils.each(gestures, function(gesture) {
+      this.element.addEventListener(gesture, handler, false);
+      this.eventHandlers.push({ gesture: gesture, handler: handler });
+    }, this);
+    return this;
+  },
+
+
+  /**
+   * unbind events to the instance
+   * @param   {String}      gesture
+   * @param   {Function}    handler
+   * @returns {Hammer.Instance}
+   */
+  off: function offEvent(gesture, handler) {
+    var gestures = gesture.split(' ')
+      , i, eh;
+    Utils.each(gestures, function(gesture) {
+      this.element.removeEventListener(gesture, handler, false);
+
+      // remove the event handler from the internal list
+      for(i=-1; (eh=this.eventHandlers[++i]);) {
+        if(eh.gesture === gesture && eh.handler === handler) {
+          this.eventHandlers.splice(i, 1);
+        }
+      }
+    }, this);
+    return this;
+  },
+
+
+  /**
+   * trigger gesture event
+   * @param   {String}      gesture
+   * @param   {Object}      [eventData]
+   * @returns {Hammer.Instance}
+   */
+  trigger: function triggerEvent(gesture, eventData) {
+    // optional
+    if(!eventData) {
+      eventData = {};
+    }
+
+    // create DOM event
+    var event = Hammer.DOCUMENT.createEvent('Event');
+    event.initEvent(gesture, true, true);
+    event.gesture = eventData;
+
+    // trigger on the target if it is in the instance element,
+    // this is for event delegation tricks
+    var element = this.element;
+    if(Utils.hasParent(eventData.target, element)) {
+      element = eventData.target;
+    }
+
+    element.dispatchEvent(event);
+    return this;
+  },
+
+
+  /**
+   * enable of disable hammer.js detection
+   * @param   {Boolean}   state
+   * @returns {Hammer.Instance}
+   */
+  enable: function enable(state) {
+    this.enabled = state;
+    return this;
+  },
+
+
+  /**
+   * dispose this hammer instance
+   * @returns {Hammer.Instance}
+   */
+  dispose: function dispose() {
+    var i, eh;
+
+    // undo all changes made by stop_browser_behavior
+    if(this.options.stop_browser_behavior) {
+      Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, true);
+    }
+
+    // unbind all custom event handlers
+    for(i=-1; (eh=this.eventHandlers[++i]);) {
+      this.element.removeEventListener(eh.gesture, eh.handler, false);
+    }
+    this.eventHandlers = [];
+
+    // unbind the start event listener
+    Event.unbindDom(this.element, Hammer.EVENT_TYPES[EVENT_START], this.eventStartHandler);
+
+    return null;
+  }
+};
+
+
+/**
+ * this holds the last move event,
+ * used to fix empty touchend issue
+ * see the onTouch event for an explanation
+ * @type {Object}
+ */
+var last_move_event = null;
+
+/**
+ * when the mouse is hold down, this is true
+ * @type {Boolean}
+ */
+var should_detect = false;
+
+/**
+ * when touch events have been fired, this is true
+ * @type {Boolean}
+ */
+var touch_triggered = false;
+
+
+var Event = Hammer.event = {
+  /**
+   * simple addEventListener
+   * @param   {HTMLElement}   element
+   * @param   {String}        type
+   * @param   {Function}      handler
+   */
+  bindDom: function(element, type, handler) {
+    var types = type.split(' ');
+    Utils.each(types, function(type){
+      element.addEventListener(type, handler, false);
+    });
+  },
+
+
+  /**
+   * simple removeEventListener
+   * @param   {HTMLElement}   element
+   * @param   {String}        type
+   * @param   {Function}      handler
+   */
+  unbindDom: function(element, type, handler) {
+    var types = type.split(' ');
+    Utils.each(types, function(type){
+      element.removeEventListener(type, handler, false);
+    });
+  },
+
+
+  /**
+   * touch events with mouse fallback
+   * @param   {HTMLElement}   element
+   * @param   {String}        eventType        like EVENT_MOVE
+   * @param   {Function}      handler
+   */
+  onTouch: function onTouch(element, eventType, handler) {
+    var self = this;
+
+
+    var bindDomOnTouch = function bindDomOnTouch(ev) {
+      var srcEventType = ev.type.toLowerCase();
+
+      // onmouseup, but when touchend has been fired we do nothing.
+      // this is for touchdevices which also fire a mouseup on touchend
+      if(Utils.inStr(srcEventType, 'mouse') && touch_triggered) {
+        return;
+      }
+
+      // mousebutton must be down or a touch event
+      else if(Utils.inStr(srcEventType, 'touch') ||   // touch events are always on screen
+        Utils.inStr(srcEventType, 'pointerdown') || // pointerevents touch
+        (Utils.inStr(srcEventType, 'mouse') && ev.which === 1)   // mouse is pressed
+        ) {
+        should_detect = true;
+      }
+
+      // mouse isn't pressed
+      else if(Utils.inStr(srcEventType, 'mouse') && !ev.which) {
+        should_detect = false;
+      }
+
+
+      // we are in a touch event, set the touch triggered bool to true,
+      // this for the conflicts that may occur on ios and android
+      if(Utils.inStr(srcEventType, 'touch') || Utils.inStr(srcEventType, 'pointer')) {
+        touch_triggered = true;
+      }
+
+      // count the total touches on the screen
+      var count_touches = 0;
+
+      // when touch has been triggered in this detection session
+      // and we are now handling a mouse event, we stop that to prevent conflicts
+      if(should_detect) {
+        // update pointerevent
+        if(Hammer.HAS_POINTEREVENTS && eventType != EVENT_END) {
+          count_touches = PointerEvent.updatePointer(eventType, ev);
+        }
+        // touch
+        else if(Utils.inStr(srcEventType, 'touch')) {
+          count_touches = ev.touches.length;
+        }
+        // mouse
+        else if(!touch_triggered) {
+          count_touches = Utils.inStr(srcEventType, 'up') ? 0 : 1;
+        }
+
+
+        // if we are in a end event, but when we remove one touch and
+        // we still have enough, set eventType to move
+        if(count_touches > 0 && eventType == EVENT_END) {
+          eventType = EVENT_MOVE;
+        }
+        // no touches, force the end event
+        else if(!count_touches) {
+          eventType = EVENT_END;
+        }
+
+        // store the last move event
+        if(count_touches || last_move_event === null) {
+          last_move_event = ev;
+        }
+
+
+        // trigger the handler
+        handler.call(Detection, self.collectEventData(element, eventType,
+                                  self.getTouchList(last_move_event, eventType),
+                                  ev) );
+
+        // remove pointerevent from list
+        if(Hammer.HAS_POINTEREVENTS && eventType == EVENT_END) {
+          count_touches = PointerEvent.updatePointer(eventType, ev);
+        }
+      }
+
+      // on the end we reset everything
+      if(!count_touches) {
+        last_move_event = null;
+        should_detect = false;
+        touch_triggered = false;
+        PointerEvent.reset();
+      }
+    };
+
+    this.bindDom(element, Hammer.EVENT_TYPES[eventType], bindDomOnTouch);
+
+    // return the bound function to be able to unbind it later
+    return bindDomOnTouch;
+  },
+
+
+  /**
+   * we have different events for each device/browser
+   * determine what we need and set them in the Hammer.EVENT_TYPES constant
+   */
+  determineEventTypes: function determineEventTypes() {
+    // determine the eventtype we want to set
+    var types;
+
+    // pointerEvents magic
+    if(Hammer.HAS_POINTEREVENTS) {
+      types = PointerEvent.getEvents();
+    }
+    // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
+    else if(Hammer.NO_MOUSEEVENTS) {
+      types = [
+        'touchstart',
+        'touchmove',
+        'touchend touchcancel'];
+    }
+    // for non pointer events browsers and mixed browsers,
+    // like chrome on windows8 touch laptop
+    else {
+      types = [
+        'touchstart mousedown',
+        'touchmove mousemove',
+        'touchend touchcancel mouseup'];
+    }
+
+    Hammer.EVENT_TYPES[EVENT_START] = types[0];
+    Hammer.EVENT_TYPES[EVENT_MOVE] = types[1];
+    Hammer.EVENT_TYPES[EVENT_END] = types[2];
+  },
+
+
+  /**
+   * create touchlist depending on the event
+   * @param   {Object}    ev
+   * @param   {String}    eventType   used by the fakemultitouch plugin
+   */
+  getTouchList: function getTouchList(ev/*, eventType*/) {
+    // get the fake pointerEvent touchlist
+    if(Hammer.HAS_POINTEREVENTS) {
+      return PointerEvent.getTouchList();
+    }
+
+    // get the touchlist
+    if(ev.touches) {
+      return ev.touches;
+    }
+
+    // make fake touchlist from mouse position
+    ev.identifier = 1;
+    return [ev];
+  },
+
+
+  /**
+   * collect event data for Hammer js
+   * @param   {HTMLElement}   element
+   * @param   {String}        eventType        like EVENT_MOVE
+   * @param   {Object}        eventData
+   */
+  collectEventData: function collectEventData(element, eventType, touches, ev) {
+    // find out pointerType
+    var pointerType = POINTER_TOUCH;
+    if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
+      pointerType = POINTER_MOUSE;
+    }
+
+    return {
+      center     : Utils.getCenter(touches),
+      timeStamp  : Date.now(),
+      target     : ev.target,
+      touches    : touches,
+      eventType  : eventType,
+      pointerType: pointerType,
+      srcEvent   : ev,
+
+      /**
+       * prevent the browser default actions
+       * mostly used to disable scrolling of the browser
+       */
+      preventDefault: function() {
+        var srcEvent = this.srcEvent;
+        srcEvent.preventManipulation && srcEvent.preventManipulation();
+        srcEvent.preventDefault && srcEvent.preventDefault();
+      },
+
+      /**
+       * stop bubbling the event up to its parents
+       */
+      stopPropagation: function() {
+        this.srcEvent.stopPropagation();
+      },
+
+      /**
+       * immediately stop gesture detection
+       * might be useful after a swipe was detected
+       * @return {*}
+       */
+      stopDetect: function() {
+        return Detection.stopDetect();
+      }
+    };
+  }
+};
+
+var PointerEvent = Hammer.PointerEvent = {
+  /**
+   * holds all pointers
+   * @type {Object}
+   */
+  pointers: {},
+
+  /**
+   * get a list of pointers
+   * @returns {Array}     touchlist
+   */
+  getTouchList: function getTouchList() {
+    var touchlist = [];
+    // we can use forEach since pointerEvents only is in IE10
+    Utils.each(this.pointers, function(pointer){
+      touchlist.push(pointer);
+    });
+
+    return touchlist;
+  },
+
+  /**
+   * update the position of a pointer
+   * @param   {String}   type             EVENT_END
+   * @param   {Object}   pointerEvent
+   */
+  updatePointer: function updatePointer(type, pointerEvent) {
+    if(type == EVENT_END) {
+      delete this.pointers[pointerEvent.pointerId];
+    }
+    else {
+      pointerEvent.identifier = pointerEvent.pointerId;
+      this.pointers[pointerEvent.pointerId] = pointerEvent;
+    }
+
+    // it's save to use Object.keys, since pointerEvents are only in newer browsers
+    return Object.keys(this.pointers).length;
+  },
+
+  /**
+   * check if ev matches pointertype
+   * @param   {String}        pointerType     POINTER_MOUSE
+   * @param   {PointerEvent}  ev
+   */
+  matchType: function matchType(pointerType, ev) {
+    if(!ev.pointerType) {
+      return false;
+    }
+
+    var pt = ev.pointerType
+      , types = {};
+
+    types[POINTER_MOUSE] = (pt === POINTER_MOUSE);
+    types[POINTER_TOUCH] = (pt === POINTER_TOUCH);
+    types[POINTER_PEN] = (pt === POINTER_PEN);
+    return types[pointerType];
+  },
+
+
+  /**
+   * get events
+   */
+  getEvents: function getEvents() {
+    return [
+      'pointerdown MSPointerDown',
+      'pointermove MSPointerMove',
+      'pointerup pointercancel MSPointerUp MSPointerCancel'
+    ];
+  },
+
+  /**
+   * reset the list
+   */
+  reset: function resetList() {
+    this.pointers = {};
+  }
+};
+
+
+var Detection = Hammer.detection = {
+  // contains all registred Hammer.gestures in the correct order
+  gestures: [],
+
+  // data of the current Hammer.gesture detection session
+  current : null,
+
+  // the previous Hammer.gesture session data
+  // is a full clone of the previous gesture.current object
+  previous: null,
+
+  // when this becomes true, no gestures are fired
+  stopped : false,
+
+
+  /**
+   * start Hammer.gesture detection
+   * @param   {Hammer.Instance}   inst
+   * @param   {Object}            eventData
+   */
+  startDetect: function startDetect(inst, eventData) {
+    // already busy with a Hammer.gesture detection on an element
+    if(this.current) {
+      return;
+    }
+
+    this.stopped = false;
+
+    // holds current session
+    this.current = {
+      inst              : inst, // reference to HammerInstance we're working for
+      startEvent        : Utils.extend({}, eventData), // start eventData for distances, timing etc
+      lastEvent         : false, // last eventData
+      lastVelocityEvent : false, // last eventData for velocity.
+      velocity          : false, // current velocity
+      name              : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
+    };
+
+    this.detect(eventData);
+  },
+
+
+  /**
+   * Hammer.gesture detection
+   * @param   {Object}    eventData
+   */
+  detect: function detect(eventData) {
+    if(!this.current || this.stopped) {
+      return;
+    }
+
+    // extend event data with calculations about scale, distance etc
+    eventData = this.extendEventData(eventData);
+
+    // hammer instance and instance options
+    var inst = this.current.inst,
+        inst_options = inst.options;
+
+    // call Hammer.gesture handlers
+    Utils.each(this.gestures, function triggerGesture(gesture) {
+      // only when the instance options have enabled this gesture
+      if(!this.stopped && inst_options[gesture.name] !== false && inst.enabled !== false ) {
+        // if a handler returns false, we stop with the detection
+        if(gesture.handler.call(gesture, eventData, inst) === false) {
+          this.stopDetect();
+          return false;
+        }
+      }
+    }, this);
+
+    // store as previous event event
+    if(this.current) {
+      this.current.lastEvent = eventData;
+    }
+
+    // end event, but not the last touch, so dont stop
+    if(eventData.eventType == EVENT_END && !eventData.touches.length - 1) {
+      this.stopDetect();
+    }
+
+    return eventData;
+  },
+
+
+  /**
+   * clear the Hammer.gesture vars
+   * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
+   * to stop other Hammer.gestures from being fired
+   */
+  stopDetect: function stopDetect() {
+    // clone current data to the store as the previous gesture
+    // used for the double tap gesture, since this is an other gesture detect session
+    this.previous = Utils.extend({}, this.current);
+
+    // reset the current
+    this.current = null;
+
+    // stopped!
+    this.stopped = true;
+  },
+
+
+  /**
+   * calculate velocity
+   * @param   {Object}  ev
+   * @param   {Number}  delta_time
+   * @param   {Number}  delta_x
+   * @param   {Number}  delta_y
+   */
+  getVelocityData: function getVelocityData(ev, delta_time, delta_x, delta_y) {
+    var cur = this.current
+      , velocityEv = cur.lastVelocityEvent
+      , velocity = cur.velocity;
+
+    // calculate velocity every x ms
+    if (velocityEv && ev.timeStamp - velocityEv.timeStamp > Hammer.UPDATE_VELOCITY_INTERVAL) {
+      velocity = Utils.getVelocity(ev.timeStamp - velocityEv.timeStamp,
+                                   ev.center.clientX - velocityEv.center.clientX,
+                                  ev.center.clientY - velocityEv.center.clientY);
+      cur.lastVelocityEvent = ev;
+    }
+    else if(!cur.velocity) {
+      velocity = Utils.getVelocity(delta_time, delta_x, delta_y);
+      cur.lastVelocityEvent = ev;
+    }
+
+    cur.velocity = velocity;
+
+    ev.velocityX = velocity.x;
+    ev.velocityY = velocity.y;
+  },
+
+
+  /**
+   * calculate interim angle and direction
+   * @param   {Object}  ev
+   */
+  getInterimData: function getInterimData(ev) {
+    var lastEvent = this.current.lastEvent
+      , angle
+      , direction;
+
+    // end events (e.g. dragend) don't have useful values for interimDirection & interimAngle
+    // because the previous event has exactly the same coordinates
+    // so for end events, take the previous values of interimDirection & interimAngle
+    // instead of recalculating them and getting a spurious '0'
+    if(ev.eventType == EVENT_END) {
+      angle = lastEvent && lastEvent.interimAngle;
+      direction = lastEvent && lastEvent.interimDirection;
+    }
+    else {
+      angle = lastEvent && Utils.getAngle(lastEvent.center, ev.center);
+      direction = lastEvent && Utils.getDirection(lastEvent.center, ev.center);
+    }
+
+    ev.interimAngle = angle;
+    ev.interimDirection = direction;
+  },
+
+
+  /**
+   * extend eventData for Hammer.gestures
+   * @param   {Object}   evData
+   * @returns {Object}   evData
+   */
+  extendEventData: function extendEventData(ev) {
+    var cur = this.current
+      , startEv = cur.startEvent;
+
+    // if the touches change, set the new touches over the startEvent touches
+    // this because touchevents don't have all the touches on touchstart, or the
+    // user must place his fingers at the EXACT same time on the screen, which is not realistic
+    // but, sometimes it happens that both fingers are touching at the EXACT same time
+    if(ev.touches.length != startEv.touches.length || ev.touches === startEv.touches) {
+      // extend 1 level deep to get the touchlist with the touch objects
+      startEv.touches = [];
+      Utils.each(ev.touches, function(touch) {
+        startEv.touches.push(Utils.extend({}, touch));
+      });
+    }
+
+    var delta_time = ev.timeStamp - startEv.timeStamp
+      , delta_x = ev.center.clientX - startEv.center.clientX
+      , delta_y = ev.center.clientY - startEv.center.clientY;
+
+    this.getVelocityData(ev, delta_time, delta_x, delta_y);
+    this.getInterimData(ev);
+
+    Utils.extend(ev, {
+      startEvent: startEv,
+
+      deltaTime : delta_time,
+      deltaX    : delta_x,
+      deltaY    : delta_y,
+
+      distance  : Utils.getDistance(startEv.center, ev.center),
+      angle     : Utils.getAngle(startEv.center, ev.center),
+      direction : Utils.getDirection(startEv.center, ev.center),
+
+      scale     : Utils.getScale(startEv.touches, ev.touches),
+      rotation  : Utils.getRotation(startEv.touches, ev.touches)
+    });
+
+    return ev;
+  },
+
+
+  /**
+   * register new gesture
+   * @param   {Object}    gesture object, see gestures.js for documentation
+   * @returns {Array}     gestures
+   */
+  register: function register(gesture) {
+    // add an enable gesture options if there is no given
+    var options = gesture.defaults || {};
+    if(options[gesture.name] === undefined) {
+      options[gesture.name] = true;
+    }
+
+    // extend Hammer default options with the Hammer.gesture options
+    Utils.extend(Hammer.defaults, options, true);
+
+    // set its index
+    gesture.index = gesture.index || 1000;
+
+    // add Hammer.gesture to the list
+    this.gestures.push(gesture);
+
+    // sort the list by index
+    this.gestures.sort(function(a, b) {
+      if(a.index < b.index) { return -1; }
+      if(a.index > b.index) { return 1; }
+      return 0;
+    });
+
+    return this.gestures;
+  }
+};
+
+
+/**
+ * Drag
+ * Move with x fingers (default 1) around on the page. Blocking the scrolling when
+ * moving left and right is a good practice. When all the drag events are blocking
+ * you disable scrolling on that area.
+ * @events  drag, drapleft, dragright, dragup, dragdown
+ */
+Hammer.gestures.Drag = {
+  name     : 'drag',
+  index    : 50,
+  defaults : {
+    drag_min_distance            : 10,
+
+    // Set correct_for_drag_min_distance to true to make the starting point of the drag
+    // be calculated from where the drag was triggered, not from where the touch started.
+    // Useful to avoid a jerk-starting drag, which can make fine-adjustments
+    // through dragging difficult, and be visually unappealing.
+    correct_for_drag_min_distance: true,
+
+    // set 0 for unlimited, but this can conflict with transform
+    drag_max_touches             : 1,
+
+    // prevent default browser behavior when dragging occurs
+    // be careful with it, it makes the element a blocking element
+    // when you are using the drag gesture, it is a good practice to set this true
+    drag_block_horizontal        : false,
+    drag_block_vertical          : false,
+
+    // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
+    // It disallows vertical directions if the initial direction was horizontal, and vice versa.
+    drag_lock_to_axis            : false,
+
+    // drag lock only kicks in when distance > drag_lock_min_distance
+    // This way, locking occurs only when the distance has become large enough to reliably determine the direction
+    drag_lock_min_distance       : 25
+  },
+
+  triggered: false,
+  handler  : function dragGesture(ev, inst) {
+    var cur = Detection.current;
+
+    // current gesture isnt drag, but dragged is true
+    // this means an other gesture is busy. now call dragend
+    if(cur.name != this.name && this.triggered) {
+      inst.trigger(this.name + 'end', ev);
+      this.triggered = false;
+      return;
+    }
+
+    // max touches
+    if(inst.options.drag_max_touches > 0 &&
+      ev.touches.length > inst.options.drag_max_touches) {
+      return;
+    }
+
+    switch(ev.eventType) {
+      case EVENT_START:
+        this.triggered = false;
+        break;
+
+      case EVENT_MOVE:
+        // when the distance we moved is too small we skip this gesture
+        // or we can be already in dragging
+        if(ev.distance < inst.options.drag_min_distance &&
+          cur.name != this.name) {
+          return;
+        }
+
+        var startCenter = cur.startEvent.center;
+
+        // we are dragging!
+        if(cur.name != this.name) {
+          cur.name = this.name;
+          if(inst.options.correct_for_drag_min_distance && ev.distance > 0) {
+            // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
+            // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
+            // It might be useful to save the original start point somewhere
+            var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
+            startCenter.pageX += ev.deltaX * factor;
+            startCenter.pageY += ev.deltaY * factor;
+            startCenter.clientX += ev.deltaX * factor;
+            startCenter.clientY += ev.deltaY * factor;
+
+            // recalculate event data using new start point
+            ev = Detection.extendEventData(ev);
+          }
+        }
+
+        // lock drag to axis?
+        if(cur.lastEvent.drag_locked_to_axis ||
+            ( inst.options.drag_lock_to_axis &&
+              inst.options.drag_lock_min_distance <= ev.distance
+            )) {
+          ev.drag_locked_to_axis = true;
+        }
+        var last_direction = cur.lastEvent.direction;
+        if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
+          // keep direction on the axis that the drag gesture started on
+          if(Utils.isVertical(last_direction)) {
+            ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
+          }
+          else {
+            ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
+          }
+        }
+
+        // first time, trigger dragstart event
+        if(!this.triggered) {
+          inst.trigger(this.name + 'start', ev);
+          this.triggered = true;
+        }
+
+        // trigger events
+        inst.trigger(this.name, ev);
+        inst.trigger(this.name + ev.direction, ev);
+
+        var is_vertical = Utils.isVertical(ev.direction);
+
+        // block the browser events
+        if((inst.options.drag_block_vertical && is_vertical) ||
+          (inst.options.drag_block_horizontal && !is_vertical)) {
+          ev.preventDefault();
+        }
+        break;
+
+      case EVENT_END:
+        // trigger dragend
+        if(this.triggered) {
+          inst.trigger(this.name + 'end', ev);
+        }
+
+        this.triggered = false;
+        break;
+    }
+  }
+};
+
+/**
+ * Hold
+ * Touch stays at the same place for x time
+ * @events  hold
+ */
+Hammer.gestures.Hold = {
+  name    : 'hold',
+  index   : 10,
+  defaults: {
+    hold_timeout  : 500,
+    hold_threshold: 2
+  },
+  timer   : null,
+
+  handler : function holdGesture(ev, inst) {
+    switch(ev.eventType) {
+      case EVENT_START:
+        // clear any running timers
+        clearTimeout(this.timer);
+
+        // set the gesture so we can check in the timeout if it still is
+        Detection.current.name = this.name;
+
+        // set timer and if after the timeout it still is hold,
+        // we trigger the hold event
+        this.timer = setTimeout(function() {
+          if(Detection.current.name == 'hold') {
+            inst.trigger('hold', ev);
+          }
+        }, inst.options.hold_timeout);
+        break;
+
+      // when you move or end we clear the timer
+      case EVENT_MOVE:
+        if(ev.distance > inst.options.hold_threshold) {
+          clearTimeout(this.timer);
+        }
+        break;
+
+      case EVENT_END:
+        clearTimeout(this.timer);
+        break;
+    }
+  }
+};
+
+/**
+ * Release
+ * Called as last, tells the user has released the screen
+ * @events  release
+ */
+Hammer.gestures.Release = {
+  name   : 'release',
+  index  : Infinity,
+  handler: function releaseGesture(ev, inst) {
+    if(ev.eventType == EVENT_END) {
+      inst.trigger(this.name, ev);
+    }
+  }
+};
+
+/**
+ * Swipe
+ * triggers swipe events when the end velocity is above the threshold
+ * for best usage, set prevent_default (on the drag gesture) to true
+ * @events  swipe, swipeleft, swiperight, swipeup, swipedown
+ */
+Hammer.gestures.Swipe = {
+  name    : 'swipe',
+  index   : 40,
+  defaults: {
+    swipe_min_touches: 1,
+    swipe_max_touches: 1,
+    swipe_velocity   : 0.7
+  },
+  handler : function swipeGesture(ev, inst) {
+    if(ev.eventType == EVENT_END) {
+      // max touches
+      if(ev.touches.length < inst.options.swipe_min_touches ||
+        ev.touches.length > inst.options.swipe_max_touches) {
+        return;
+      }
+
+      // when the distance we moved is too small we skip this gesture
+      // or we can be already in dragging
+      if(ev.velocityX > inst.options.swipe_velocity ||
+        ev.velocityY > inst.options.swipe_velocity) {
+        // trigger swipe events
+        inst.trigger(this.name, ev);
+        inst.trigger(this.name + ev.direction, ev);
+      }
+    }
+  }
+};
+
+/**
+ * Tap/DoubleTap
+ * Quick touch at a place or double at the same place
+ * @events  tap, doubletap
+ */
+Hammer.gestures.Tap = {
+  name    : 'tap',
+  index   : 100,
+  defaults: {
+    tap_max_touchtime : 250,
+    tap_max_distance  : 10,
+    tap_always        : true,
+    doubletap_distance: 20,
+    doubletap_interval: 300
+  },
+
+  has_moved: false,
+
+  handler : function tapGesture(ev, inst) {
+    var prev, since_prev, did_doubletap;
+
+    // reset moved state
+    if(ev.eventType == EVENT_START) {
+      this.has_moved = false;
+    }
+
+    // Track the distance we've moved. If it's above the max ONCE, remember that (fixes #406).
+    else if(ev.eventType == EVENT_MOVE && !this.moved) {
+      this.has_moved = (ev.distance > inst.options.tap_max_distance);
+    }
+
+    else if(ev.eventType == EVENT_END &&
+        ev.srcEvent.type != 'touchcancel' &&
+        ev.deltaTime < inst.options.tap_max_touchtime && !this.has_moved) {
+
+      // previous gesture, for the double tap since these are two different gesture detections
+      prev = Detection.previous;
+      since_prev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
+      did_doubletap = false;
+
+      // check if double tap
+      if(prev && prev.name == 'tap' &&
+          (since_prev && since_prev < inst.options.doubletap_interval) &&
+          ev.distance < inst.options.doubletap_distance) {
+        inst.trigger('doubletap', ev);
+        did_doubletap = true;
+      }
+
+      // do a single tap
+      if(!did_doubletap || inst.options.tap_always) {
+        Detection.current.name = 'tap';
+        inst.trigger(Detection.current.name, ev);
+      }
+    }
+  }
+};
+
+/**
+ * Touch
+ * Called as first, tells the user has touched the screen
+ * @events  touch
+ */
+Hammer.gestures.Touch = {
+  name    : 'touch',
+  index   : -Infinity,
+  defaults: {
+    // call preventDefault at touchstart, and makes the element blocking by
+    // disabling the scrolling of the page, but it improves gestures like
+    // transforming and dragging.
+    // be careful with using this, it can be very annoying for users to be stuck
+    // on the page
+    prevent_default    : false,
+
+    // disable mouse events, so only touch (or pen!) input triggers events
+    prevent_mouseevents: false
+  },
+  handler : function touchGesture(ev, inst) {
+    if(inst.options.prevent_mouseevents &&
+        ev.pointerType == POINTER_MOUSE) {
+      ev.stopDetect();
+      return;
+    }
+
+    if(inst.options.prevent_default) {
+      ev.preventDefault();
+    }
+
+    if(ev.eventType == EVENT_START) {
+      inst.trigger(this.name, ev);
+    }
+  }
+};
+
+
+/**
+ * Transform
+ * User want to scale or rotate with 2 fingers
+ * @events  transform, pinch, pinchin, pinchout, rotate
+ */
+Hammer.gestures.Transform = {
+  name     : 'transform',
+  index    : 45,
+  defaults : {
+    // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
+    transform_min_scale      : 0.01,
+    // rotation in degrees
+    transform_min_rotation   : 1,
+    // prevent default browser behavior when two touches are on the screen
+    // but it makes the element a blocking element
+    // when you are using the transform gesture, it is a good practice to set this true
+    transform_always_block   : false,
+    // ensures that all touches occurred within the instance element
+    transform_within_instance: false
+  },
+
+  triggered: false,
+
+  handler  : function transformGesture(ev, inst) {
+    // current gesture isnt drag, but dragged is true
+    // this means an other gesture is busy. now call dragend
+    if(Detection.current.name != this.name && this.triggered) {
+      inst.trigger(this.name + 'end', ev);
+      this.triggered = false;
+      return;
+    }
+
+    // at least multitouch
+    if(ev.touches.length < 2) {
+      return;
+    }
+
+    // prevent default when two fingers are on the screen
+    if(inst.options.transform_always_block) {
+      ev.preventDefault();
+    }
+
+    // check if all touches occurred within the instance element
+    if(inst.options.transform_within_instance) {
+      for(var i=-1; ev.touches[++i];) {
+        if(!Utils.hasParent(ev.touches[i].target, inst.element)) {
+          return;
+        }
+      }
+    }
+
+    switch(ev.eventType) {
+      case EVENT_START:
+        this.triggered = false;
+        break;
+
+      case EVENT_MOVE:
+        var scale_threshold = Math.abs(1 - ev.scale);
+        var rotation_threshold = Math.abs(ev.rotation);
+
+        // when the distance we moved is too small we skip this gesture
+        // or we can be already in dragging
+        if(scale_threshold < inst.options.transform_min_scale &&
+          rotation_threshold < inst.options.transform_min_rotation) {
+          return;
+        }
+
+        // we are transforming!
+        Detection.current.name = this.name;
+
+        // first time, trigger dragstart event
+        if(!this.triggered) {
+          inst.trigger(this.name + 'start', ev);
+          this.triggered = true;
+        }
+
+        inst.trigger(this.name, ev); // basic transform event
+
+        // trigger rotate event
+        if(rotation_threshold > inst.options.transform_min_rotation) {
+          inst.trigger('rotate', ev);
+        }
+
+        // trigger pinch event
+        if(scale_threshold > inst.options.transform_min_scale) {
+          inst.trigger('pinch', ev);
+          inst.trigger('pinch' + (ev.scale<1 ? 'in' : 'out'), ev);
+        }
+        break;
+
+      case EVENT_END:
+        // trigger dragend
+        if(this.triggered) {
+          inst.trigger(this.name + 'end', ev);
+        }
+
+        this.triggered = false;
+        break;
+    }
+  }
+};
+
+// AMD export
+if(typeof define == 'function' && define.amd) {
+  define(function(){
+    return Hammer;
+  });
+}
+// commonjs export
+else if(typeof module == 'object' && module.exports) {
+  module.exports = Hammer;
+}
+// browser export
+else {
+  window.Hammer = Hammer;
+}
+
+})(window);
\ No newline at end of file
diff --git a/src/ext/string-lerp.js b/src/ext/string-lerp.js
new file mode 100644 (file)
index 0000000..5473199
--- /dev/null
@@ -0,0 +1,307 @@
+/* string-lerp - progressively turn one string into another
+   Copyright 2014 Joe Wreschnig
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/string-lerp/
+*//*
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+   General Public License for more details.
+
+   As additional permission, you may distribute the program or works
+   based on it without the copy of the GNU GPL normally required,
+   provided you include this license notice and a URL through which
+   recipients can access the corresponding source code.
+*/
+
+/*globals exports, Uint32Array */
+
+(function (exports) {
+    "use strict";
+
+    var MAX_MATRIX_SIZE = 256 * 256;
+
+    function costMatrix(source, target, ins, del, sub) {
+        /** Calculate the Levenshtein cost matrix for source and target
+
+            If source and target are strings, they cannot contain any
+            astral or combining codepoints. Such data must be passed
+            as arrays of strings with one element per glyph.
+
+            ins, del, and sub are the costs for insertion, deletion,
+            and substition respectively. Their default value is 1. If
+            only ins is passed, del and sub are set to the same cost.
+            If ins and del are passed, sub is set to the more
+            expensive of the two.
+
+            The matrix is returned as a flat typed array.
+
+            Following http://en.wikipedia.org/wiki/Levenshtein_distance
+        */
+        ins = ins === undefined ? 1 : (ins | 0);
+        del = (del | 0) || ins;
+        sub = (sub | 0) || Math.max(ins, del);
+        var m = source.length + 1;
+        var n = target.length + 1;
+        var d = new Uint32Array(m * n);
+        var i, j;
+        for (i = 1; i < m; ++i)
+            d[n * i] = i;
+        for (j = 1; j < n; ++j)
+            d[j] = j;
+        for (j = 1; j < n; ++j)
+            for (i = 1; i < m; ++i)
+                if (source[i - 1] === target[j - 1])
+                    d[n * i + j] = d[n * (i - 1) + j - 1];
+                else
+                    d[n * i + j] = Math.min(del + d[n * (i - 1) + j    ],
+                                            ins + d[n *    i    + j - 1],
+                                            sub + d[n * (i - 1) + j - 1]);
+        return d;
+    }
+
+    // First, note that deletion is just substition with nothing, so
+    // any DEL operation can be replaced by a SUB. Second, the
+    // operation code *is* the necessary slice offset for applying the
+    // diff.
+    var INS = 0, SUB = 1;
+
+    function editPath(costs, target) {
+        /** Given a cost matrix and a target, create an edit list */
+        var path = [];
+        var j = target.length;
+        var n = j + 1;
+        var i = costs.length / n - 1;
+        while (i || j) {
+            var sub = (i && j) ? costs[n * (i - 1) + j - 1] : Infinity;
+            var del = i ? costs[n * (i - 1) + j] : Infinity;
+            var ins = j ? costs[n * i + j - 1] : Infinity;
+            if (sub <= ins && sub <= del) {
+                if (costs[n * i + j] !== costs[n * (i - 1) + j - 1])
+                    path.push([SUB, i - 1, target[j - 1]]);
+                --i; --j;
+            } else if (ins <= del) {
+                path.push([INS, i, target[j - 1]]);
+                --j;
+            } else {
+                path.push([SUB, i - 1, ""]);
+                --i;
+            }
+        }
+        return path;
+    }
+
+    function diff(source, target, ins, del, sub) {
+        /** Create a list of edits to turn source into target
+
+            ins, del, and sub are as passed to costMatrix.
+        */
+        return editPath(costMatrix(source, target, ins, del, sub), target);
+    }
+
+    function patchArray(diff, source) {
+        for (var i = 0; i < diff.length; ++i) {
+            var edit = diff[i];
+            source.splice(edit[1], edit[0], edit[2]);
+        }
+        return source;
+    }
+
+    function patchString(diff, source) {
+        for (var i = 0; i < diff.length; ++i) {
+            var edit = diff[i];
+            var head = source.slice(0, edit[1]);
+            var tail = source.slice(edit[1] + edit[0]);
+            source = head + edit[2] + tail;
+        }
+        return source;
+    }
+
+    function patch(diff, source) {
+        /** Apply a list of edits to source */
+        var patcher = Array.isArray(source) ? patchArray : patchString;
+        return patcher(diff, source);
+    }
+
+    // Matches if a string contains combining characters or astral
+    // codepoints (technically, the first half surrogate of an astral
+    // codepoint).
+    var MULTI = /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uD800-\uDBFF\uFE20-\uFE2F]/;
+
+    // Match an entire (potentially astral) codepoint and any
+    // combining characters following it.
+    var GLYPH = /[\0-\u02FF\u0370-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uD800-\uFE1F\uFE30-\uFFFF][\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uDC00-\uDFFF\uFE20-\uFE2F]*/g;
+
+    function diffLerpAstral(source, target, amount) {
+        // This split is not perfect for all languages, but at least
+        // it won't create invalid surrogate pairs or orphaned
+        // combining characters.
+        var sourceGlyphs = source.match(GLYPH) || [];
+        var targetGlyphs = target.match(GLYPH) || [];
+        var edits = diff(targetGlyphs, sourceGlyphs, 2, 2, 3);
+        // The edit path works from the string end, forwards, because
+        // that's how Levenshtein edits work. To match LTR reading
+        // direction (and the behavior of fastLerp), swap the strings
+        // and invert the parameter when editing.
+        var partial = edits.slice(0, Math.round((1 - amount) * edits.length));
+        return patchArray(partial, targetGlyphs).join("");
+    }
+
+    function diffLerpBasic(source, target, amount) {
+        var edits = diff(target, source, 2, 2, 3);
+        // The edit path works from the string end, forwards, because
+        // that's how Levenshtein edits work. To match LTR reading
+        // direction (and the behavior of fastLerp), swap the strings
+        // and invert the parameter when editing.
+        var partial = edits.slice(0, Math.round((1 - amount) * edits.length));
+        return patchString(partial, target);
+    }
+
+    function diffLerp(source, target, amount) {
+        /** Interpolate between two strings using edit operations
+
+            This interpolation algorithm applys a partial edit of one
+            string into the other. This produces nice looking results,
+            but can take a significant amount of time and memory to
+            compute the edits. It is not recommended for strings
+            longer than a few hundred characters.
+        */
+
+        if (source.match(MULTI) || target.match(MULTI))
+            return diffLerpAstral(source, target, amount);
+        else
+            return diffLerpBasic(source, target, amount);
+    }
+
+    var NUMBERS = /(-?\d{1,20}(?:\.\d{1,20})?)/g;
+
+    function areNumericTwins(source, target) {
+        /** Check if a and b differ only in numerals */
+        return source.replace(NUMBERS, "0") === target.replace(NUMBERS, "0");
+    }
+
+    function nlerp(source, target, amount) {
+        return source + (target - source) * amount;
+    }
+
+    function numericLerp(source, target, amount) {
+        /** Interpolate numerically between strings containing numbers
+
+            Numbers may have a leading "-" and a single "." to mark
+            the decimal point, but something must be after the ".".
+            No other floating point syntax (e.g. 1e6) is supported.
+            They are treated as fixed-point values, with the point's
+            position itself interpolating.
+
+            For example, numericLerp("0.0", "100".0, 0.123) === "12.3"
+            because the "." in "0.0" is interpreted as a decimal
+            point. But numericLerp("0.", "100.", 0.123) === "12."
+            because the strings are interpreted as integers followed
+            by a full stop.
+
+            Calling this functions on strings that differ in more than
+            numerals gives undefined results.
+        */
+
+        var targetParts = target.split(NUMBERS);
+        var match;
+        var i = 1;
+        while ((match = NUMBERS.exec(source))) {
+            var sourcePart = match[0];
+            var targetPart = targetParts[i];
+            var part = nlerp(+sourcePart, +targetPart, amount);
+            var sourcePoint = sourcePart.indexOf(".");
+            var targetPoint = targetPart.indexOf(".");
+            var point = Math.round(nlerp(
+                sourcePoint >= 0 ? (sourcePart.length - 1) - sourcePoint : 0,
+                targetPoint >= 0 ? (targetPart.length - 1) - targetPoint : 0,
+                amount));
+            targetParts[i] = part.toFixed(point);
+            i += 2;
+        }
+        return targetParts.join("");
+    }
+
+    function fastLerpAstral(source, target, amount) {
+        var sourceGlyphs = source.match(GLYPH) || [];
+        var targetGlyphs = target.match(GLYPH) || [];
+        var sourceLength = Math.round(sourceGlyphs.length * amount);
+        var targetLength = Math.round(targetGlyphs.length * amount);
+        var head = targetGlyphs.slice(0, targetLength);
+        var tail = sourceGlyphs.slice(sourceLength, sourceGlyphs.length);
+        head.push.apply(head, tail);
+        return head.join("");
+    }
+
+    function fastLerpBasic(source, target, amount) {
+        var sourceLength = Math.round(source.length * amount);
+        var targetLength = Math.round(target.length * amount);
+        var head = target.substring(0, targetLength);
+        var tail = source.substring(sourceLength, source.length);
+        return head + tail;
+    }
+
+    function fastLerp(source, target, amount) {
+        /** Interpolate between two strings based on length
+
+            This interpolation algorithm progressively replaces the
+            front of one string with another. This approach is fast
+            but does not look good when the strings are similar.
+        */
+
+        // TODO: Consider fast-pathing this even more for very large
+        // strings, e.g. in the megabyte range. These are large enough
+        // that it should be fine to just pick a codepoint and search
+        // for the nearest glyph start.
+        if (source.match(MULTI) || target.match(MULTI))
+            return fastLerpAstral(source, target, amount);
+        else
+            return fastLerpBasic(source, target, amount);
+    }
+
+    function lerp(source, target, amount) {
+        /** Interpolate between two strings as best as possible
+
+            If the strings are identical aside from numbers in them,
+            they are passed through numericLerp.
+
+            If the strings are not numbers and short, they are passed
+            through diffLerp.
+
+            Otherwise, they are passed through fastLerp.
+        */
+        source = source.toString();
+        target = target.toString();
+
+        // Fast path for boundary cases.
+        if (amount === 0) return source;
+        if (amount === 1) return target;
+
+        if (areNumericTwins(source, target))
+            return numericLerp(source, target, amount);
+
+        // Numeric lerps should over- and under-shoot when fed numbers
+        // outside 0 to 1, but other types cannot.
+        if (amount < 0) return source;
+        if (amount > 1) return target;
+
+        var n = source.length * target.length;
+        var appropriate = (n && n < MAX_MATRIX_SIZE) ? diffLerp : fastLerp;
+        return appropriate(source, target, amount);
+    }
+
+    exports.costMatrix = costMatrix;
+    exports.patch = patch;
+    exports.diff = diff;
+    exports.fastLerp = fastLerp;
+    exports.diffLerp = diffLerp;
+    exports.numericLerp = numericLerp;
+    exports.lerp = lerp;
+
+})(typeof exports === "undefined" ? (this.stringLerp = {}) : exports);
diff --git a/src/index.html b/src/index.html
new file mode 100644 (file)
index 0000000..1fedd23
--- /dev/null
@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+<html lang="en"
+      data-appid="com.yukkurigames.pwl6">
+  <head>
+    <title>Pixel Witch Lesson #6</title>
+    <meta charset="utf-8" />
+    <meta name="viewport"
+          content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-status-bar-style"
+          content="black-translucent" />
+    <link rel=icon href=data/images/icons.ico
+          sizes="16x16 32x32 64x64 128x128 256x256"
+          type="image/vnd.microsoft.icon">
+    <link rel=icon href=data/images/icons.icns
+          sizes="16x16 32x32 64x64 128x128 256x256"
+          >
+    <link rel="stylesheet" href="yuu/data/yuu.css" type="text/css" />
+    <link rel="stylesheet" href="pwl6.css" type="text/css" />
+    <script type="text/javascript" src="ext/gl-matrix.js"></script>
+    <script type="text/javascript" src="ext/gamepad.js"></script>
+    <script type="text/javascript" src="ext/hammer.js"></script>
+    <script type="text/javascript" src="ext/string-lerp.js"></script>
+    <script type="text/javascript" src="yuu/pre.js"></script>
+    <script type="text/javascript" src="yuu/yf.js"></script>
+    <script type="text/javascript" src="yuu/yT.js"></script>
+    <script type="text/javascript" src="yuu/core.js"></script>
+    <script type="text/javascript" src="yuu/input.js"></script>
+    <script type="text/javascript" src="yuu/ce.js"></script>
+    <script type="text/javascript" src="yuu/gfx.js"></script>
+    <script type="text/javascript" src="yuu/rdr.js"></script>
+    <script type="text/javascript" src="yuu/audio.js"></script>
+    <script type="text/javascript" src="yuu/director.js"></script>
+    <script type="text/javascript" src="yuu/storage.js"></script>
+    <script type="text/javascript" src="pwl6.js"></script>
+  </head>
+  
+  <body>
+    <canvas id="yuu-canvas" data-yuu-resize>
+    </canvas>
+    <div id="yuu-error" class="yuu-overlay"
+         data-yuu-animation="yuu-from-top"
+         data-yuu-dismiss-key="escape">
+      <div data-yuu-command="dismiss" tabindex=0></div>
+      <p>There was a problem. Sorry about that.</p>
+      <p id="yuu-error-message"></p>
+      <h2>Error Log</h2>
+      <pre id="yuu-error-stack">
+      </pre>
+    </div>
+    <div id="yuu-fatal-error" class="yuu-overlay">
+      <p>There was a serious problem. You'll have to restart. Sorry
+      about that.</p>
+      <p id="yuu-fatal-error-message"></p>
+      <p>
+        Supported browsers include recent versions of
+        <a href="http://www.mozilla.org/firefox/">Mozilla Firefox</a>
+        and <a href="http://www.google.com/chrome/">Google Chrome</a>
+        on most desktop computers,
+        <a href="https://www.google.com/intl/en/chrome/browser/mobile/android.html">Chrome
+          for Android</a>, and Safari on Mac OS X 10.7 and later
+        <a href="https://discussions.apple.com/thread/3300585?start=0">if
+          you enable WebGL manually</a>.
+      </p>
+      <h2>Error Log</h2>
+      <pre id="yuu-fatal-error-stack">
+      </pre>
+    </div>
+    <div id="preferences" class="yuu-overlay"
+         data-yuu-animation="yuu-from-top-right"
+         data-yuu-dismiss-key="f10 escape">
+      <div data-yuu-command="dismiss" tabindex=0></div>
+      <h1>Pixel Witch Lesson #6</h1>
+      <table class="yuu-options">
+        <tr>
+          <td>
+            <input type="checkbox" data-yuu-command="mute" id="mute">
+            <label for="mute" title="Toggle audio mute (Control+S)"></label>
+          </td>
+          <td>
+            <label for="volume">Volume</label>
+          </td>
+          <td>
+            <input type="range" data-yuu-command="volume" id="volume"
+                   min="0" max="1.0" step="0.05" style="width: 95%">
+          </td>
+        </tr>
+        <tr>
+          <td>
+          </td>
+          <td>
+            <label for="volume">Music</label>
+          </td>
+          <td>
+            <input type="range" data-yuu-command="musicVolume" id="musicVolume"
+                   min="0" max="1.0" step="0.05" style="width: 95%">
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <input type="checkbox" data-yuu-command="fullscreen"
+                   id="fullscreen">
+            <label for="fullscreen" title="Toggle fullscreen (F11)"></label>
+          </td>
+          <td colspan=2>
+            <label for="fullscreen">Fullscreen</label>
+          </td>
+        </tr>
+      </table>
+      <p style="text-align: center">Press F12 to take a screenshot.</p>
+    </div>
+    <div id="colophon" class="yuu-overlay"
+         data-yuu-animation="yuu-from-top"
+         data-yuu-dismiss-key="escape">
+      <div data-yuu-command="dismiss" tabindex=0></div>
+      <h1><a href="http://yukkurigames.com/pwl6/">Pixel Witch Lesson #6</a></h1>
+      <dl>
+        <dt>Designed &amp; Implemented</dt>
+        <dd>Joe Wreschnig</dd>
+        <dt>Additional Programming</dt>
+        <dd>
+          Brandon Jones &amp; Colin MacKenzie IV
+          (<a href="http://glmatrix.net/">glMatrix</a>)
+        </dd>
+        <dd>
+          Christoph Burgmer
+          (<a href="https://github.com/cburgmer/ayepromise">ayepromise</a>)
+        </dd>
+        <dd>
+          Ian McEwan, Ashima Arts
+          (<a href="https://github.com/ashima/webgl-noise">WebGL Noise</a>)
+        </dd>
+        <dd>
+          Jorik Tangelder
+          (<a href="http://eightmedia.github.io/hammer.js/">Hammer.js</a>)
+        </dd>
+        <dt>Fonts</dt>
+        <dd>
+          Carrois Type Design
+          (<a href="http://www.carrois.com/en/fira-3-1/">Fira</a>)
+        </dd>
+        <dd>
+          Dave Gandy
+          (<a href="http://fortawesome.github.io/">Font Awesome</a>)
+        </dd>
+        <dt>Special Thanks</dt>
+        <dd>Amelia Gorman</dd>
+        <dd>Jessicatz Fairymeadow</dd>
+        <dd><a href="http://www.kenney.nl/">Kenney.nl</a></dd>
+        <dd>
+          Richard
+          <a href="http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html">"The Clock Guy"</a>
+          Oliver
+        </dd>
+      </dl>
+      <hr>
+      <div style="text-align: center">
+        <p>
+          Copyright &copy;2014
+          <a href="http://yukkurigames.com/">Yukkuri Games</a>
+          and others
+        </p>
+        <p>
+          This program is free software; you can redistribute it and/or
+          modify it under the terms of the GNU General Public License as
+          published by the Free Software Foundation; either 
+          <a href="http://www.gnu.org/licenses/gpl-2.0.html">version
+            2 of the License</a>, or (at your option)
+          <a href="https://www.gnu.org/copyleft/gpl.html">any later version</a>.
+        </p>
+      </div>
+      <hr>
+      <p id="yuu-licensing" data-yuu-command="licensing">
+        View Complete Licensing Text
+      </p>
+    </div>
+
+    <noscript>
+      <div class="yuu-overlay" style="display: block">
+        <p>
+          This game requires JavaScript to play. It's a fun game, and
+          we hope you take your time to enable JS and try it.
+        </p>
+        <p>
+          We promise it doesn't contain any remote tracking cookies or
+          bugs, load any external scripts, or do any of the things
+          you've probably disabled JavaScript to avoid.
+        </p>
+        <p>
+          No, we don't even load the things people lie about when they
+          say they aren't tracking you: No Google APIs, no content
+          distribution network proxies, no analytics scripts. Just the
+          game.
+        </p>
+        <hr>
+        <ul class="link-footer">
+          <li><a href="http://yukkurigames.com/pwl6/">Pixel Witch Lesson #6</a>
+          <li><a href="http://yukkurigames.com/">Yukkuri Games</a>
+        </ul>
+      </div>
+    </noscript>
+  </body>
+</html>
diff --git a/src/pwl6.css b/src/pwl6.css
new file mode 100644 (file)
index 0000000..2b55cb0
--- /dev/null
@@ -0,0 +1,65 @@
+body {
+    background-color: black;
+}
+
+.yuu-toast {
+    background-color: hsla(276, 33%, 10%, 1.0);
+    border: solid hsla(276, 33%, 48%, 1.0) 2px;
+    color: hsla(276, 33%, 95%, 1.0);
+}
+
+.yuu-overlay {
+    background-color: hsla(276, 33%, 10%, 0.9);
+    border: solid hsla(276, 33%, 48%, 1.0) 2px;
+    color: hsla(276, 33%, 95%, 1.0);
+}
+
+.yuu-overlay *:focus {
+    box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    -moz-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    -webkit-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    outline: none;
+}
+
+input[type=range][data-yuu-command] {
+    background-color: hsla(276, 33%, 25%, 1.0);
+    border: solid hsla(276, 33%, 48%, 0.33) 1px;
+}
+
+input[type=range][data-yuu-command]::-webkit-slider-thumb {
+    background-color: hsla(276, 33%, 65%, 1.0);
+}
+
+input[type=range][data-yuu-command]::-moz-range-track {
+    background-color: hsla(276, 33%, 25%, 1.0);
+}
+
+input[type=range][data-yuu-command]::-moz-range-thumb {
+    background: hsla(276, 33%, 65%, 1.0);
+}
+
+input[type=checkbox][data-yuu-command]:focus + label[for] {
+    box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    -moz-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    -webkit-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0);
+    outline: 0;
+}
+
+input[data-yuu-command] + label[for]:before {
+    color: hsla(276, 33%, 65%, 1.0);
+}
+
+a[href], [data-yuu-command] {
+    color: hsla(276, 33%, 65%, 1.0);
+}
+
+hr {
+    border: solid hsla(276, 33%, 48%, 0.75) 1px;
+}
+
+.link-footer {
+    list-style-type: none;
+    text-align: center;
+    margin-left: 0;
+    padding-left: 0;
+}
diff --git a/src/pwl6.js b/src/pwl6.js
new file mode 100644 (file)
index 0000000..dc50206
--- /dev/null
@@ -0,0 +1,2149 @@
+"use strict";
+
+var storage;
+var SIGILS;
+var BOOK;
+var handScene;
+var circleScene;
+var NOISY_BLOCKS;
+
+var sounds;
+
+var TOP = 0;
+var LEFT = 1;
+var BOTTOM = 2;
+var RIGHT = 3;
+
+var MenuScene, CircleScene, HandScene, BookScene, GridScene;
+
+yuu.Texture.DEFAULTS.magFilter = yuu.Texture.DEFAULTS.minFilter = "nearest";
+
+var TICK_ROT = quat.rotateZ(quat.create(), quat.create(), Math.PI / 30);
+var TICK_REV = quat.invert(quat.create(), TICK_ROT);
+var TICK_ROT2 = quat.rotateZ(quat.create(), quat.create(), Math.PI / 60);
+
+function sawtooth (p) {
+    /** Sawtooth wave, Ã› = 1, T = 2Ï€, f(0) = 0, f′(0) > 0 */
+    var _2PI = 2 * Math.PI;
+    return 2 * (p / _2PI - Math.floor(0.5 + p / _2PI));
+}
+
+function triangle (p) {
+    /** Triangle wave, Ã› = 1, T = 2Ï€, f(0) = 0, f′(0) > 0 */
+    return 2 * Math.abs(sawtooth(p + Math.PI / 2)) - 1;
+}
+
+function waveshift (period, peak, xoffset, yoffset) {
+    period /= 2 * Math.PI;
+    xoffset = xoffset || 0;
+    yoffset = yoffset || 0;
+    return function (f) {
+        return function (p) {
+            return yoffset + peak * f.call(this, (p + xoffset) / period);
+        };
+    };
+}
+
+function cycler (scale) {
+    var f = waveshift(scale, 0.5, -Date.now(), 0.5)(triangle);
+    return function () { return f(Date.now()); };
+}
+
+function load () {
+    storage = ystorage.getStorage();
+    yuu.audio.storage = storage;
+
+    NOISY_BLOCKS = new yuu.ShaderProgram(null, ["@noise.glsl", "@noisyblocks"]);
+
+    SIGILS = new yuu.Material("@sigils");
+    BOOK = new yuu.Material("@book", NOISY_BLOCKS);
+    BOOK.uniforms.cut = yf.volatile(cycler(20000));
+    BOOK.uniforms.range = 0.06;
+    BOOK.texture.ready.then(function (texture) {
+        BOOK.uniforms.resolution = new Float32Array(
+            [texture.width / 4, texture.height / 4]);
+    });
+
+    sounds = {
+        tick: new yuu.Instrument("@tick"),
+        tock: new yuu.Instrument("@tock"),
+        regear: new yuu.Instrument("@regear"),
+        winding: new yuu.Instrument("@winding"),
+        slam: new yuu.Instrument("@slam"),
+        switch: new yuu.Instrument("@switch"),
+        clicking: new yuu.Instrument("@clicking"),
+        bookAppear: new yuu.Instrument("@book-appear"),
+        switchBroke: new yuu.Instrument({
+            sample: { "@switch": { duration: 0.27, offset: 0.1 } } }),
+        switchOn: new yuu.Instrument({
+            sample: { "@switch": { duration: 0.2 } } }),
+        switchOff: new yuu.Instrument({
+            sample: { "@switch": { offset: 0.2 } } }),
+        chime: new yuu.Instrument({
+            envelope: { "0": 1, "0.7": 0.2, "3": 0 },
+            modulator: {
+                envelope: { "0": 1, "0.7": 0.2, "3": 0 },
+                frequency: "x1.5",
+            }
+        }),
+    };
+
+    yuu.director.pushScene(circleScene = new CircleScene());
+    yuu.director.pushScene(handScene = new HandScene());
+    yuu.director.pushScene(new MenuScene());
+    if (!storage.getFlag("instructions")) {
+        yuu.director.entity0.attach(new yuu.Ticker(function () {
+            yuu.director.pushScene(new BookScene());
+        }, 60));
+    }
+
+    return yuu.ready(
+        [SIGILS, BOOK]
+        .concat(yf.map(yf.getter.bind(sounds), Object.keys(sounds)))
+    );
+}
+
+function start () {
+    yuu.director.start();
+}
+
+window.addEventListener("load", function() {
+    yuu.registerInitHook(load);
+    yuu.init({ backgroundColor: [0, 0, 0, 1], antialias: false }).then(start);
+});
+
+var PALETTE = [[ 0.76, 0.13, 0.13 ],
+               [ 0.33, 0.49, 0.71 ],
+               [ 0.45, 0.68, 0.32 ],
+               [ 0.51, 0.32, 0.63 ],
+               [ 0.89, 0.49, 0.11 ],
+               [ 1.00, 1.00, 0.30 ]];
+
+var LEVELS = [
+    { name: "12345654321",
+      randomSlammer: [3, 5],
+      deps: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
+    },
+    
+    { slammer: [1, 1], sets: "tutorial",
+      scramble: { easy: "01", hard: "0122" } },
+    { slammer: [1, 1, 1], deps: "tutorial",
+      scramble: { easy: "11", hard: "1212" } },
+    { slammer: [2, 1], deps: "tutorial", sets: "asymmetric",
+      scramble: { easy: "32", hard: "3321" } },
+    { slammer: [1, 2, 1], deps: "tutorial", sets: "unequal",
+      scramble: { easy: "112", hard: "3210" } },
+    { slammer: [2, 0], deps: "asymmetric", sets: "zero",
+      scramble: { easy: "23", hard: "032" } },
+    { slammer: [2, 0, 2], deps: "zero",
+      scramble: { easy: "11", hard: "2211" } },
+    { slammer: [1, 1, 1, 1], deps: "tutorial" },
+    { slammer: [2, 1, 1], deps: "asymmetric" },
+    { slammer: [1, 2, 1, 2], deps: "asymmetric",
+      scramble: { easy: "012" } },
+    { slammer: [1, 2, 3, 4], deps: "asymmetric", sets: "solid",
+      scramble: { easy: "110" } },
+    { slammer: [5, 0, 0, 4, 0], deps: "unequal zero",
+      scramble: { easy: "112" } },
+    { slammer: [5, 3, 5, 3, 5], deps: "unequal solid",
+      scramble: { easy: "3232" } },
+    { slammer: [4, 4, 4, 0, 4], deps: "solid zero",
+      scramble: { easy: "0321" } },
+    { slammer: [1, 3, 0, 2], deps: "unequal zero" },
+    { slammer: [1, 2, 3, 2, 1], deps: "unequal",
+      scramble: { easy: "3333" } },
+    { slammer: [2, 0, 1, 2, 4], deps: "unequal zero" },
+];
+
+function levelName (level) {
+    return (level.name || level.slammer.join("")).trim();
+}
+
+function wonLevel (level, difficulty) {
+    if (level.sets)
+        storage.setFlag(level.sets);
+    storage.setFlag(levelName(level) + "@" + difficulty);
+}
+
+function hasBeaten (level, difficulty) {
+    return storage.getFlag(levelName(level) + "@" + difficulty);
+}
+
+function scrambleForLevel (rnd, level, difficulty) {
+    var c = difficulty === "easy" ? 0 : 1;
+    if (difficulty === "random")
+        c = rnd.randrange(2, 5);
+    var length = level.slammer.length;
+    return rnd.randrange(length * c, length * (c + 1)) + 2;
+}
+
+function difficultyForLevel (level) {
+    if (level.deps && !level.deps.split(" ").every(storage.getFlag, storage))
+        return null;
+    if (hasBeaten(level, "hard"))
+        return "random";
+    if (hasBeaten(level, "easy"))
+        return "hard";
+    else
+        return "easy";
+}
+
+function levelRandom (level, difficulty) {
+    if (difficulty === "random")
+        return yuu.random;
+    else
+        return new yuu.Random(yuu.createLCG(+level.slammer.join("")));
+}
+
+function generateBoard (rnd, level) {
+    var size = level.length;
+    var board = new Array(size);
+    for (var i = 0; i < size; ++i)
+        board[i] = yf.repeat(i % PALETTE.length + 1, size);
+    if (rnd.randbool())
+        yuu.transpose2d(board);
+    return board;
+}
+
+function generateSlammer (rnd, level) {
+    var s = new Array(level.length);
+    for (var i = 0; i < s.length; ++i)
+        s[i] = yf.repeat(0, level[i]);
+    if (rnd.randbool())
+        s.reverse();
+    return s;
+}
+
+var AnimationQueue = yT(yuu.C, {
+    constructor: function () {
+        this._queue = [];
+    },
+
+    attached: function () {
+        this._queue = [];
+    },
+
+    _runNext: function () {
+        var next = this._queue[0];
+        if (next && this.entity)
+            this.entity.attach(new yuu.Animation(
+                next.timeline, next.params, this._complete.bind(this)));
+    },
+
+    _complete: function () {
+        var next = this._queue.shift();
+        next.resolve();
+        this._runNext();
+    },
+
+    enqueue: function (timeline, params) {
+        return new Promise(function (resolve) {
+            this._queue.push({
+                timeline: timeline,
+                params: params,
+                resolve: resolve
+            });
+            // FIXME: Simply chaining the promise doesn't work here
+            // because the tick between the two handlers is often long
+            // enough to render a frame, and that frame will have some
+            // undesirable intermediate state.
+            if (this._queue.length === 1)
+                this._runNext();
+        }.bind(this));
+    },
+
+    SLOTS: ["animationQueue"]
+});
+
+var SLAMMER_ROTATE = {
+    0: { tween1: { yaw: "yaw" }, duration: 10 }
+};
+
+var ROTATE_ALL = {
+    0: { tweenAll: { yaw: "yaws" }, duration: 10 }
+};
+
+var SLAMMER_BOUNCE = {
+    0: { tween1: { y: 0.5 }, duration: 5, repeat: -1 }
+};
+
+var SLIDE_BLOCKS = {
+    0: { tweenAll: { position: "positions" },
+         duration: 8, easing: "linear" },
+};
+
+var SLAMMER_SLAM = {
+    0:  { tween1: { y: -1.5 }, easing: "linear", duration: 6 },
+    6:  { event: "slideBoardBlocks" },
+    15: { event: "slam",
+          tween1: { y: 0 }, easing: "linear", duration: 8 }
+};
+     
+var GRID_DISMISS = {
+    0: { tween1: { yaw: 2 * Math.PI, x: "x", y: "y", scale: [0.3, 0.3, 1] },
+         duration: 45 }
+};
+
+var GRID_FINISHED = {
+    0: { tween: { arm: { scale: [0, 0, 1], yaw: "armYaw", y: "armY" },
+                  board: { y: "boardY" } },
+         duration: 45 }
+};
+
+function rotateCw (d) { return (--d + 4) % 4; }
+function rotateCcw (d) { return ++d % 4; }
+function opposite (d) { return (d + 2) % 4; }
+
+var FlagSet = yT({
+    /** Manage a set of semaphore-like counting flags. */
+
+    constructor: function () {
+        /** Construct a flag set for the provided flags.
+
+            Flags are initialized to 0 by default.
+        */
+        this._counts = {};
+        for (var i = 0; i < arguments.length; ++i)
+            this._counts[arguments[i]] = 0;
+    },
+
+    increment: function () {
+        /** Increment the provided flags. */
+        for (var i = 0; i < arguments.length; ++i)
+            this._counts[arguments[i]]++;
+    },
+
+    decrement: function () {
+        /** Decrement the provided flags.
+
+            No underflow checks are performed. A flag with a negative
+            value is considered set exactly as a flag with a positive
+            value.
+        */
+        for (var i = 0; i < arguments.length; ++i)
+            this._counts[arguments[i]]--;
+    },
+
+    some: function () {
+        /** Return true if any of the provided flags are set. */
+        return yf.some.call(this._counts, yf.getter, arguments);
+    },
+
+    every: function () {
+        /** Return true if all of the provided flags are set. */
+        return yf.every.call(this._counts, yf.getter, arguments);
+    },
+
+    none: function () {
+        /** Return true if none of the provided flags are set. */
+        return !this.some.apply(this, arguments);
+    },
+
+    incrementer: function () {
+        /** Provide a bound 0-ary function to increment the provided flags.
+
+            Useful for wrapps around context-free callbacks.
+        */
+        var that = this, args = arguments;
+        return function () { that.increment.apply(that, args); };
+    },
+
+    decrementer: function () {
+        /** Provide a bound 0-ary function to decrement the provided flags.
+
+            Useful for wrapps around context-free callbacks.
+        */
+        var that = this, args = arguments;
+        return function () { that.decrement.apply(that, args); };
+    }
+});
+
+var BoardController = yT(yuu.C, {
+    constructor: function (rnd, level, colors) {
+        this.contents = generateBoard(rnd, level.slammer);
+        this.colors = colors;
+    },
+    updateChildren: function () {
+        this.entity.data.quads.forEach(function (q) {
+            q.quad.position = [q.x, q.y];
+            var i = this.contents[q.x][q.y];
+            q.quad.color = this.colors[i];
+            q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
+        }, this);
+    },
+    isComplete: function() {
+        var x, y;
+        var rows = true, cols = true;
+        for (x = 1; x < this.contents.length && rows; ++x)
+            for (y = 0; y < this.contents[x].length && rows; ++y)
+                rows = this.contents[x - 1][y] === this.contents[x][y];
+        for (x = 0; x < this.contents.length && cols; ++x)
+            for (y = 1; y < this.contents[x].length && cols; ++y)
+                cols = this.contents[x][y - 1] === this.contents[x][y];
+        return rows || cols;
+    },
+
+    shift: [
+        function (x, replacement) {
+            var lost = this.contents[x].pop();
+            this.contents[x].unshift(replacement);
+            return lost;
+        },
+        function (y, replacement) {
+            yuu.transpose2d(this.contents);
+            var lost = this.shift[BOTTOM].call(this, y, replacement);
+            yuu.transpose2d(this.contents);
+            return lost;
+        },
+        function (x, replacement) {
+            var lost = this.contents[x].shift();
+            this.contents[x].push(replacement);
+            return lost;
+        },
+        function (y, replacement) {
+            yuu.transpose2d(this.contents);
+            var lost = this.shift[TOP].call(this, y, replacement);
+            yuu.transpose2d(this.contents);
+            return lost;
+        }
+    ],
+
+    SLOTS: ["controller"]
+});
+
+var SlammerController = yT(yuu.C, {
+    constructor: function (rnd, level, colors) {
+        this.blocks = generateSlammer(rnd, level.slammer);
+        this.orientation = TOP;
+        this.colors = colors;
+        this._undoRecord = [];
+    },
+    isComplete: function() {
+        return yf.none(yf.some.bind(null, null), this.blocks);
+    },
+    updateChildren: function () {
+        this.entity.data.quads.forEach(function (q) {
+            var i = this.blocks[q.x][q.y];
+            q.quad.position = [q.x, q.y];
+            q.quad.color = this.colors[i];
+            q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
+        }, this);
+    },
+
+    lastUndoRecord: { get: function () {
+        return yf.last(this._undoRecord);
+    } },
+
+    clearUndoRecord: function () {
+        this._undoRecord = [];
+    },
+    
+    slam: function (board) {
+        var undoable = (this.orientation !== this.lastUndoRecord);
+        var length = this.blocks.length;
+        this.orientation = opposite(this.orientation);
+        this.blocks = yf.mapr.call(this, function (a, y) {
+            return yf.map(board.shift[this.orientation].bind(board, y), a)
+                .reverse();
+        }, this.blocks, (this.orientation & 2)
+                        ? yf.range(length)
+                        : yf.range(length - 1, -1, -1));
+        yf.each(function (i) {
+            i.x = length - (i.x + 1);
+        }, this.entity.data.quads);
+        this.updateChildren();
+        board.updateChildren();
+        if (undoable)
+            this._undoRecord.push(this.orientation);
+        else
+            this._undoRecord.pop();
+    },
+
+    SLOTS: ["controller"]
+});
+
+function randSide (rnd, except) {
+    return (rnd || yuu.random).choice(
+        yf.without([TOP, LEFT, BOTTOM, RIGHT], except));
+}
+
+var HANDS_LEFT = {
+    0: { tween: { left: { yaw: -0.3 } }, duration: 3 },
+    3: { tween: { left: { yaw: 0.0 } }, duration: 7 },
+};
+
+var HANDS_RIGHT = {
+    0: { tween: { right: { yaw: -0.3 } }, duration: 3 },
+    3: { tween: { right: { yaw: 0.0 } }, duration: 7 },
+};
+
+var HANDS_UNDO = {
+    0: { tween: { left: { yaw: 0.2 }, right: { yaw: 0.2 } },
+         duration: 3 },
+    3: { tween: { left: { yaw: 0.0 }, right: { yaw: 0.0 } },
+         duration: 7 }
+};
+
+var HANDS_MENU_CHOICE = {
+    0: { tween: { left:  { x: -1.3 },
+                  right: { x: -1.3 } },
+         duration: 15, easing: "ease_in"
+       },
+
+    10: { tween: { left:  { scaleX: 1 },
+                   right: { scaleX: 1 } },
+          duration: 20 },
+
+    20: { set: { leftQuad:  { color: "frontColor" },
+                 rightQuad: { color: "frontColor" } },
+          tween: { left: { x: 0 }, right: { x: 0 } },
+          duration: 15
+        },
+};
+
+var HANDS_RETURN = {
+    0: { tween: { left:  { x: -1.3 },
+                  right: { x: -1.3 } },
+         duration: 20
+       },
+
+    10: { tween: { left:  { scaleX: -1 },
+                   right: { scaleX: -1 } },
+          duration: 20 },
+
+    20: { set: { leftQuad:  { color: "backColor" },
+                 rightQuad: { color: "backColor" } },
+          tween: { left: { x: -1 }, right: { x: -1 } },
+          duration: 10
+        },
+};
+
+var HANDS_SLAM = [
+    // TOP
+    { 0: { tween: { left:  { yaw: -0.2, scaleX: 0.8, y: -0.1 },
+                    right: { yaw: -0.2, scaleX: 0.8, y: -0.1 },
+                  }, duration: 10, repeat: -1 },
+    },
+
+    // LEFT
+    { 0: { tween: { left:  { scaleX: 0.8, x: 0.1 },
+                    right: { scaleX: 0.9 },
+                  }, duration: 10, repeat: -1 },
+    },
+
+    // BOTTOM
+    { 0: { tween: { left:  { yaw: 0.2, scaleX: 0.8 },
+                    right: { yaw: 0.2, scaleX: 0.8 },
+                  }, duration: 10, repeat: -1 },
+    },
+
+    // RIGHT
+    { 0: { tween: { left:  { scaleX: 0.9 },
+                    right: { scaleX: 0.8, x: 0.1 },
+                  }, duration: 10, repeat: -1 },
+    },
+];
+
+var HANDS_ROTATE_CW = {
+    0: { tween: { left: { scaleX: 0.8 } }, duration: 5 },
+    5: { tween: { left: { scaleX: 1.0 } }, duration: 5 },
+};
+
+var HANDS_ROTATE_CCW = {
+    0: { tween: { right: { scaleX: 0.8 } }, duration: 5 },
+    5: { tween: { right: { scaleX: 1 } }, duration: 5 },
+};
+
+var BUTTONS_IN = {
+    0: { tween: { a: { x: 0 }, b: { x: 1.5 } }, duration: 25 }
+};
+
+var BUTTONS_OUT = {
+    0: { tween: { a: { x: -1.5 }, b: { x: 0 } }, duration: 25 }
+};
+
+HandScene = yT(yuu.Scene, {
+    constructor: function () {
+        yuu.Scene.call(this);
+        var hands = new yuu.Material("@hand");
+        this.left = new yuu.E(new yuu.Transform());
+        var l = new yuu.E(
+            new yuu.Transform([-0.5, 0.5, 0]),
+            new yuu.DataC({ command: "left" }),
+            this.leftQuad = new yuu.QuadC(hands));
+        this.left.addChild(l);
+        var r = new yuu.E(
+            new yuu.Transform([-0.5, 0.5, 0]),
+            new yuu.DataC({ command: "right" }),
+            this.rightQuad = new yuu.QuadC(hands));
+        this.right = new yuu.E(new yuu.Transform());
+        this.right.addChild(r);
+        var SIZE_X = yuu.random.gauss(1.2, 0.15) * 0.35;
+        var SIZE_Y = yuu.random.gauss(1.1, 0.05) * 0.51;
+        var hand = yuu.random.randrange(3);
+        this.leftQuad.texBounds = this.rightQuad.texBounds = [
+            hand / 2.99, 0, (hand + 1) / 3.01, 1];
+        this.layer0.resize(-0.75, 0, 1.5, 1.5);
+        var leftWrist = new yuu.E(
+            new yuu.Transform([-0.20, 0, 0], null,
+                              [SIZE_X, SIZE_Y, 1]));
+        var rightWrist = new yuu.E(
+            new yuu.Transform([0.20, 0, 0], null,
+                              [-SIZE_X, SIZE_Y, 1]));
+        leftWrist.addChild(this.left);
+        rightWrist.addChild(this.right);
+        this.addEntities(leftWrist, rightWrist);
+        this.backColor = yuu.hslToRgb(
+            (yuu.random.gauss(0.1, 0.1) + 10) % 1,
+            yuu.random.uniform(0.2, 0.7),
+            yuu.random.uniform(0.2, 0.6),
+            1.0);
+        this.leftQuad.alpha = this.rightQuad.alpha = 0.2;
+        var hsl = yuu.rgbToHsl(this.backColor);
+        hsl[2] = hsl[2].lerp(1, 0.15);
+        hsl[1] = hsl[1].lerp(0, 0.30);
+        hsl[3] = 0.4;
+        this.frontColor = yuu.hslToRgb(hsl);
+        this.leftQuad.color = this.rightQuad.color = this.frontColor;
+        this.ready = hands.ready;
+
+        function Button (i, command) {
+            return new yuu.E(
+                new yuu.Transform(),
+                new yuu.DataC({ command: command }),
+                new yuu.QuadC(SIGILS)
+                    .setTexBounds([i / 6, 0, (i + 1) / 6, 0.5])
+                    .setColor(PALETTE[i]));
+        }
+
+        this.helpButton = new Button(1, "help");
+        this.backButton = new Button(3, "back");
+        this.backButton.transform.x -= 1.5;
+        this.leftButtons = new yuu.E(new yuu.Transform());
+        this.leftButtons.addChildren(this.helpButton, this.backButton);
+        this.rightButton = new Button(2, "showOverlay preferences");
+        this.leftButtons.transform.scale
+            = this.rightButton.transform.scale
+            = [0.075, 0.075, 1];
+        this.entity0.addChildren(this.leftButtons, this.rightButton);
+        this.buttons = [this.helpButton, this.backButton, this.rightButton,
+                        l, r];
+     },
+
+    inputs: {
+        resize: function () {
+            var base = new yuu.AABB(-0.75, 0, 0.75, 1.5);
+            var vp = base.matchAspectRatio(yuu.viewport);
+            vp.y1 -= vp.y0;
+            vp.y0 = 0;
+            this.leftButtons.transform.xy = [
+                vp.x0 + this.leftButtons.transform.scaleX,
+                vp.y1 - this.leftButtons.transform.scaleY];
+            this.rightButton.transform.xy = [
+                vp.x1 - this.rightButton.transform.scaleX,
+                vp.y1 - this.rightButton.transform.scaleY];
+            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
+        },
+
+        mousemove: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            this.cursor = "";
+            for (var i = 0; i < this.buttons.length; ++i) {
+                if (this.buttons[i].transform.contains(p)) {
+                    this.cursor = "pointer";
+                }
+            }
+        },
+
+        tap: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            for (var i = 0; i < this.buttons.length; ++i) {
+                if (this.buttons[i].transform.contains(p)) {
+                    yuu.director.execute(this.buttons[i].data.command);
+                    return true;
+                }
+            }
+        },
+
+        doubletap: function () {
+            return this.inputs.tap.apply(this, arguments);
+        },
+    },
+
+    _anim: function (timeline) {
+        this.entity0.attach(new yuu.Animation(
+            timeline, {
+                left: this.left.transform,
+                right: this.right.transform,
+                leftQuad: this.leftQuad,
+                rightQuad: this.rightQuad,
+                frontColor: this.frontColor,
+                backColor: this.backColor
+            }));
+    },
+
+    undo: function () { this._anim(HANDS_UNDO); },
+    movedLeft: function () { this._anim(HANDS_LEFT); },
+    movedRight: function () { this._anim(HANDS_RIGHT); },
+    slam: function (o) { this._anim(HANDS_SLAM[o]); },
+    rotatedCw: function () { this._anim(HANDS_ROTATE_CW); },
+    rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW); },
+    menuChoice: function () {
+        this.entity0.attach(new yuu.Animation(
+            BUTTONS_IN, {
+                a: this.backButton.transform,
+                b: this.helpButton.transform
+            }));
+        this._anim(HANDS_MENU_CHOICE);
+    },
+    finished: function () {
+        this.entity0.attach(new yuu.Animation(
+            BUTTONS_OUT, {
+                a: this.backButton.transform,
+                b: this.helpButton.transform
+            }));
+        this._anim(HANDS_RETURN);
+    },
+});
+
+var GRID_APPEAR = {
+    0: { set1:   { y: 5 },
+         tween1: { y: 0 }, duration: 10 },
+};
+
+GridScene = yT(yuu.Scene, {
+    constructor: function (level, difficulty) {
+        yuu.Scene.call(this);
+        this.entity0.attach(new yuu.Transform());
+        this.level = level;
+        this.difficulty = difficulty;
+        this._locks = new FlagSet("slam", "spin", "quit");
+        var rnd = levelRandom(level, difficulty);
+        var colors = yuu.random.shuffle(PALETTE.slice());
+        colors.unshift([1.0, 1.0, 1.0]);
+        this.board = new yuu.E(new BoardController(rnd, level, colors),
+                               new yuu.Transform(),
+                               new yuu.DataC({ quads: [] }));
+        this.slammer = new yuu.E(new SlammerController(rnd, level, colors),
+                                 new yuu.Transform(),
+                                 new yuu.DataC({ quads: [] }));
+        this.slammerHead = new yuu.E(new yuu.Transform());
+        this.slammerRoot = new yuu.E(new yuu.Transform());
+        var length = level.slammer.length;
+        var maxSize = length * length;
+        var slammerBatch = new yuu.QuadBatchC(maxSize);
+        slammerBatch.material = SIGILS;
+        var boardBatch = new yuu.QuadBatchC(maxSize);
+        boardBatch.material = SIGILS;
+        this.slammerRoot.transform.xy = [length / 2 - 0.5, length / 2 - 0.5];
+        this.slammerHead.transform.xy = [-length / 2 + 0.5, length / 2 + 2];
+        this.slammerRoot.addChild(this.slammerHead);
+        this.slammerHead.addChild(this.slammer);
+        this.slammer.attach(slammerBatch);
+        this.board.attach(boardBatch);
+        yf.irange.call(this, function (x) {
+            yf.irange.call(this, function (y) {
+                var quad = boardBatch.createQuad();
+                quad.color = colors[this.board.controller.contents[x][y]];
+                quad.position = [x, y];
+                this.board.data.quads.push({ quad: quad, x: x, y: y });
+            }, length);
+        }, length);
+
+        for (var x = 0; x < this.slammer.controller.blocks.length; ++x) {
+            for (var y = 0; y < this.slammer.controller.blocks[x].length; ++y) {
+                var quad = slammerBatch.createQuad();
+                quad.color = colors[this.slammer.controller.blocks[x][y]];
+                quad.position = [x, y];
+                this.slammer.data.quads.push({ quad: quad, x: x, y: y });
+            }
+        }
+        this.addEntities(this.board, this.slammerRoot);
+        this.scramble(rnd);
+        if (!(this.cheating = yuu.director.input.pressed["`"])) {
+            this.slammer.controller.clearUndoRecord();
+        }
+        this._dragging = 0;
+
+        this.gridBB = new yuu.AABB(-0.5, -0.5, length - 0.5, length - 0.5);
+        this.leftBB = new yuu.AABB(
+            -Infinity, this.gridBB.y0, this.gridBB.x0, this.gridBB.y1);
+        this.rightBB = new yuu.AABB(
+            this.gridBB.x1, this.gridBB.y0, Infinity, this.gridBB.y1);
+        this.topBB = new yuu.AABB(
+            this.gridBB.x0, this.gridBB.y1, this.gridBB.x1, Infinity);
+        this.bottomBB = new yuu.AABB(
+            this.gridBB.x0, -Infinity, this.gridBB.x1, this.gridBB.y0);
+    },
+
+    init: function () {
+        this._locks.increment("slam");
+        this.entity0.attach(new yuu.Animation(
+            GRID_APPEAR, { $: this.slammer.transform },
+            this._locks.decrementer("slam")));
+    },
+
+    scramble: function (rnd) {
+        var scramble = (this.level.scramble || {})[this.difficulty];
+        var slammerCon = this.slammer.controller;
+        var boardCon = this.board.controller;
+        if (!scramble) {
+            var count = scrambleForLevel(rnd, this.level, this.difficulty);
+            while (this.isComplete()) {
+                var c = count;
+                while (c--) {
+                    slammerCon.orientation = randSide(rnd, slammerCon.orientation);
+                    slammerCon.slam(boardCon);
+                }
+            }
+        } else {
+            for (var i =0; i < scramble.length; ++i) {
+                slammerCon.orientation = +scramble[i];
+                slammerCon.slam(boardCon);
+            }
+        }
+        slammerCon.orientation = randSide();
+        this.slammerRoot.transform.yaw = slammerCon.orientation * Math.PI / 2;
+    },
+
+    isComplete: function () {
+        return this.slammer.controller.isComplete()
+            && this.board.controller.isComplete();
+    },
+
+    rotateTo: yuu.cmd(function (orientation) {
+        return new Promise(function (resolve) {
+            if (this._locks.some("spin"))
+                return this;
+            this.slammer.controller.orientation = orientation;
+            this._locks.increment("slam");
+            var yaw0 = this.slammerRoot.transform.yaw;
+            var yaw1 = orientation * Math.PI / 2;
+
+            sounds.clicking.play();
+            this.entity0.attach(new yuu.Animation(
+                SLAMMER_ROTATE, {
+                    $: this.slammerRoot.transform,
+                    yaw: yaw0 + yuu.normalizeRadians(yaw1 - yaw0)
+                }, function () {
+                    this._locks.decrement("slam");
+                    resolve(this);
+                }.bind(this)));
+        }.bind(this));
+    }, "<top/bottom/left/right>", "move the slammer to the top"),
+
+    rotateCw: yuu.cmd(function () {
+        handScene.rotatedCw();
+        circleScene.rotated();
+        this.rotateTo(rotateCw(this.slammer.controller.orientation));
+    }, "", "rotate the active piece clockwise"),
+
+    rotateCcw: yuu.cmd(function () {
+        handScene.rotatedCcw();
+        circleScene.rotated();
+        this.rotateTo(rotateCcw(this.slammer.controller.orientation));
+    }, "", "rotate the active piece counter-clockwise"),
+
+    left: yuu.cmd(function () { this.rotateCw(); }),
+    right: yuu.cmd(function () { this.rotateCcw(); }),
+
+    undo: yuu.cmd(function (v) {
+        var con = this.slammer.controller;
+        var _ = function () { this.undo(this._undo); }.bind(this);
+        if ((this._undo = v) && con.lastUndoRecord !== undefined) {
+            if (con.orientation !== con.lastUndoRecord) {
+                circleScene.reverse();
+                handScene.undo();
+                this.rotateTo(con.lastUndoRecord)
+                    .then(this.slam.bind(this))
+                    .then(_);
+            } else {
+                this.slam().then(_);
+            }
+        }
+    }, "", "rotate the active piece counter-clockwise"),
+
+    checkWon: function () {
+        if (this.isComplete() && !this._locks.some("quit")) {
+            this._locks.increment("quit", "slam", "spin");
+            var firstTime = !hasBeaten(this.level, this.difficulty);
+            if (!this.cheating)
+                wonLevel(this.level, this.difficulty);
+            var scene = new MenuScene(this.level);
+            yuu.director.pushScene(scene);
+            scene.didWinLevel(this.level, this.difficulty, firstTime);
+            this.entity0.attach(new yuu.Animation(
+                GRID_FINISHED, {
+                    arm: this.slammerRoot.transform,
+                    armYaw: this.slammerRoot.transform.yaw + 3 * Math.PI,
+                    armY: this.slammerRoot.transform.y + 1.5,
+                    board: this.board.transform,
+                    boardY: this.level.slammer.length * 3
+                }, function () {
+                    yuu.director.removeScene(this);
+                }.bind(this)
+            ));
+        }
+    },
+
+    slideBoardBlocks: function (anim, params) {
+        var dx = 0, dy = 0;
+        var orientation = this.slammer.controller.orientation;
+        switch (orientation) {
+        case LEFT: dx = 1.5; break;
+        case TOP: dy = -1.5; break;
+        case RIGHT: dx = -1.5; break;
+        case BOTTOM: dy = 1.5; break;
+        }
+        var sgnx = Math.sign(dx);
+        var sgny = Math.sign(dy);
+        var $s = [];
+        var positions = [];
+        var blocks = this.slammer.controller.blocks;
+        this.slammer.data.quads.forEach(function (q) {
+            var d = blocks[q.x].length;
+            $s.push(q.quad);
+            positions.push([q.quad.position[0], q.quad.position[1] - d]);
+        }, this);
+        this.board.data.quads.forEach(function (q) {
+            var x = orientation === TOP ? q.x : blocks.length - (q.x + 1);
+            var y = orientation === LEFT ? q.y : blocks.length - (q.y + 1);
+
+            $s.push(q.quad);
+            positions.push([q.quad.position[0] + sgnx * blocks[y].length,
+                            q.quad.position[1] + sgny * blocks[x].length]);
+        }, this);
+        this.entity0.attach(new yuu.Animation(SLIDE_BLOCKS, {
+            $s: $s,
+            positions: positions
+        }));
+    },
+
+    slam: yuu.cmd(function () {
+        var r = new Promise(function (resolve, reject) {
+            if (this._locks.some("slam")) {
+                reject("slamming is locked");
+                return;
+            }
+            this._locks.increment("spin", "slam");
+            circleScene.slam();
+            sounds.slam.play();
+            handScene.slam(this.slammer.controller.orientation);
+            this.entity0.attach(new yuu.Animation(
+                SLAMMER_SLAM, {
+                    $: this.slammer.transform,
+                    slam: function () {
+                        this._locks.decrement("spin");
+                        this.slammer.controller.slam(this.board.controller);
+                        this.slammerRoot.transform.yaw = Math.PI / 2 *
+                            this.slammer.controller.orientation;
+                    }.bind(this),
+                    slideBoardBlocks: this.slideBoardBlocks.bind(this)
+                }, function () {
+                    this.checkWon();
+                    this._locks.decrement("slam");
+                    resolve(this);
+                }.bind(this)));
+        }.bind(this));
+        return r;
+    }, "", "slam the active piece"),
+
+    back: yuu.cmd(function (x, y) {
+        if (this._locks.some("quit"))
+            return;
+        this._locks.increment("quit", "slam", "spin");
+        var scene = new MenuScene(this.level);
+        yuu.director.pushScene(scene);
+        var v = [x || yuu.random.uniform(-1, 1),
+                 y || yuu.random.uniform(-1, 1)];
+        var size = this.board.controller.contents.length * 5;
+        vec2.scale(v, vec2.normalize(v, v), size);
+        this.entity0.attach(new yuu.Animation(
+            GRID_DISMISS, {
+                $: this.entity0.transform,
+                x: v[0], y: v[1]
+            }, function () {
+                yuu.director.removeScene(this);
+            }.bind(this)
+        ));
+        circleScene.lose();
+    }, "", "go back to the menu"),
+
+    slammerBB: { get: function (p) {
+        var length = this.level.slammer.length;
+        switch (this.slammer.controller.orientation) {
+        case LEFT:
+            return new yuu.AABB(-Infinity, -0.5, -1, length - 0.5);
+        case RIGHT:
+            return new yuu.AABB(length + 1, -0.5, Infinity, length - 0.5);
+        case TOP:
+            return new yuu.AABB(-0.5, length + 1, length - 0.5, Infinity);
+        case BOTTOM:
+            return new yuu.AABB(-0.5, -Infinity, length - 0.5, -1);
+        }
+    } },
+
+    _swipe: function (p0, p1) {
+        p0 = this.layer0.worldFromDevice(p0);
+        p1 = this.layer0.worldFromDevice(p1);
+        if (this.slammerBB.contains(p0)) {
+            this.slam();
+            return true;
+        }
+        if (this.gridBB.contains(p0) && !this.gridBB.contains(p1)) {
+            this.back(p1.x - p0.x, p1.y - p0.y);
+            return true;
+        }
+    },
+
+    inputs: {
+        resize: function () {
+            var length = this.level.slammer.length;
+            var base = new yuu.AABB(-length - 2.5, -length - 2.5,
+                                    2 * length + 1.5, 2 * length + 1.5);
+            var vp = base.matchAspectRatio(yuu.viewport);
+            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
+        },
+
+        tap: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (this.gridBB.contains(p)) {
+                this.slam();
+                return true;
+            }
+        },
+
+        touch: function (p) {
+            var length = this.level.slammer.length;
+            var middle = (length - 1) / 2;
+            p = this.layer0.worldFromDevice(p);
+            if (this.slammerBB.contains(p)) {
+                this.slammer.attach(new yuu.Animation(
+                    SLAMMER_BOUNCE, { $: this.slammer.transform }));
+            } else if (this.leftBB.contains(p)) {
+                this.rotateTo(LEFT);
+                handScene.rotatedCw();
+                return true;
+            } else if (this.rightBB.contains(p)) {
+                this.rotateTo(RIGHT);
+                handScene.rotatedCcw();
+                return true;
+            } else if (this.topBB.contains(p)) {
+                this.rotateTo(TOP);
+                if (p.x < middle)
+                    handScene.rotatedCw();
+                else
+                    handScene.rotatedCcw();
+                return true;
+            } else if (this.bottomBB.contains(p)) {
+                this.rotateTo(BOTTOM);
+                if (p.x < middle)
+                    handScene.rotatedCw();
+                else
+                    handScene.rotatedCcw();
+                return true;
+            }
+        },
+
+        doubletap: function () {
+            return this.inputs.tap.apply(this, arguments);
+        },
+
+        hold: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (this.gridBB.contains(p)) {
+                this.undo(true);
+                return true;
+            }
+        },
+
+        dragstart: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            this._dragging = this.slammerBB.contains(p);
+        },
+
+        drag: function (p0, p1) {
+            var p = this.layer0.worldFromDevice(p1);
+            if (this._dragging && !this._locks.some("slam")) {
+                var inGrid = this.gridBB.contains(p);
+                var length = this.level.slammer.length;
+                var o;
+                if (this._dragging === true && inGrid) {
+                    this.slam();
+                } else if (p.x > 0 && p.x < length && !inGrid) {
+                    o = p.y < 0 ? BOTTOM : TOP;
+                    if (o !== this.slammer.controller.orientation) {
+                        this.rotateTo(o);
+                        this._dragging = 2;
+                    }
+                } else if (p.y > 0 && p.y < length && !inGrid) {
+                    o = p.x < 0 ? LEFT : RIGHT;
+                    if (o !== this.slammer.controller.orientation) {
+                        this.rotateTo(o);
+                        this._dragging = 2;
+                    }
+                }
+            }
+            return this._dragging;
+        },
+
+        dragend: function (p0, p1) {
+            this._dragging = false;
+        },
+
+        release: function () {
+            this.undo(false);
+        },
+
+        swipeleft: function (p0, p1) {
+            return this._swipe(p0, p1);
+        },
+        swiperight: function (p0, p1) {
+            return this._swipe(p0, p1);
+        },
+        swipeup: function (p0, p1) {
+            return this._swipe(p0, p1);
+        },
+        swipedown: function (p0, p1) {
+            return this._swipe(p0, p1);
+        },
+    },
+
+    KEYBINDS: {
+        w: "rotateCw",
+        a: "rotateCcw",
+        s: "rotateCcw",
+        d: "rotateCw",
+        left: "rotateCcw",
+        right: "rotateCw",
+        up: "rotateCw",
+        down: "rotateCcw",
+        shift: "slam",
+        space: "slam",
+        z: "slam",
+        backspace: "+undo",
+        c: "+undo",
+        escape: "back",
+        back: "back",
+        gamepadbutton0: "slam",
+        gamepadbutton1: "+undo",
+        gamepadbutton2: "slam",
+        gamepadbutton3: "+undo",
+        gamepadbutton4: "rotateCcw",
+        gamepadbutton5: "rotateCw",
+        gamepadbutton8: "back",
+        gamepadbutton14: "rotateCcw",
+        gamepadbutton15: "rotateCw",
+    }
+});
+
+var MENU_APPEAR = {
+    0: [{ set1:   { x: 5, y: 5, scaleX: 0, scaleY: 0 } },
+        { tween1: { x: 0, y: 0, scaleX: 1 }, duration: 24 },
+        { tween1: { scaleY: 1 },
+          duration: 55, easing: yuu.Tween.METASPRING(1, 10)}],
+};
+
+var MENU_SLIDE = {
+    0: { tween1: { x: "x" }, duration: "duration" }
+};
+
+var FLASH = {
+    0:  { tween1: { luminance: 1, alpha: 1 }, duration: 32, repeat: -1 }
+};
+
+var MENU_SLAM = {
+    0:  { tween: { cursor: { y: "mid" } }, duration: 8 },
+    8:  { tween: { cursor: { y: "line" },
+                   select: { y: -1.0 }
+                 }, duration: 12 },
+    20: { tween: { scene: { y: 10 },
+                   select: { y: -11.5, scale: [3, 3, 1] }
+                 }, duration: 18 },
+    38: { event: "appear",
+          tween: { select: { y: 0, scale: [1, 0, 1] } },
+          duration: 20 }
+};
+
+function menuEntityForLevel (level, i) {
+    var activated = false;
+    function randomizeSlammer () {
+        var min = level.randomSlammer[0];
+        var max = level.randomSlammer[1];
+        var size = yuu.random.randrange(min, max + 1);
+        level.slammer = [];
+        do {
+            for (var i = 0; i < size; ++i)
+                level.slammer[i] = yuu.random.randrange(0, size);
+        } while (Math.min.apply(Math, level.slammer) === max
+                || Math.max.apply(Math, level.slammer) === 0);
+    }
+
+    function generateQuads() {
+        batch.disposeAll();
+        var rgb = PALETTE[i % PALETTE.length];
+        var fit = level.slammer.length + 1;
+        batch.createQuad().color = rgb;
+        level.slammer.forEach(function (size, y) {
+            var c = batch.createQuad();
+            c.color = [0, 0, 0];
+            c.alpha = hasBeaten(level, "easy") ? 0.5 : 1.0;
+            c.size = [size / fit, 1 / fit];
+            c.position = [0, -0.5 + (y + 1) / fit];
+        });
+        ce.data.flasher = batch.createQuad();
+        ce.data.flasher.alpha = 0;
+        return ce;
+    }
+
+    if (level.randomSlammer)
+        randomizeSlammer();
+
+    // 14 = maximum slammer size + 1 background + 1 flasher
+    var batch = new yuu.QuadBatchC(14);
+    var ce = new yuu.E(
+        new yuu.Transform([2 * i, 0, 0]),
+        batch,
+        new yuu.DataC({
+            activate: function () {
+                activated = true;
+                var scene = new GridScene(level, difficultyForLevel(level));
+                yuu.director.insertUnderScene(scene);
+            }
+        })
+    );
+
+    if (level.randomSlammer) {
+        ce.attach(new yuu.Ticker(function () {
+            if (!activated && yuu.random.randbool(0.7)) {
+                randomizeSlammer();
+                ++i;
+                generateQuads();
+            }
+            return !activated;
+        }, 30, 15));
+    }
+
+    generateQuads();
+    return ce;
+}
+
+var HAND_TICK_BACK = {
+    0: { tween1: { rotation: "rotation" }, duration: 6, repeat: -1 }
+};
+
+MenuScene = yT(yuu.Scene, {
+    constructor: function (initialLevel) {
+        yuu.Scene.call(this);
+        this.entity0.attach(new yuu.Transform(),
+                            new AnimationQueue());
+
+        this.pointer = new yuu.E(
+            new yuu.Transform([5, 8, 0]),
+            new yuu.QuadC());
+
+        var menu = this.menu = new yuu.E(new yuu.Transform([5, 6.5, 0]));
+        this.addEntities(menu, this.pointer);
+        this.availableLevels = LEVELS.filter(difficultyForLevel);
+        this.availableLevels
+            .map(menuEntityForLevel)
+            .forEach(menu.addChild, menu);
+
+        var initialIdx = this.availableLevels.indexOf(initialLevel);
+        this._locks = new FlagSet("slam", "move");
+        this.activeIndex = Math.max(initialIdx, 0);
+        menu.transform.x = 5 - 2 * this.activeIndex;
+        this.changeActiveIndex(this.activeIndex, false);
+        this._dragStartX = null;
+
+        this.entity0.attach(
+            new yuu.Ticker(this._animation.bind(this), 60));
+    },
+
+    _animation: function (count) {
+        var length = this.availableLevels.length;
+        var range = Math.pow(2, length);
+        var rand = yuu.random.randrange(range);
+        var targets = [];
+        var yaws = [];
+        for (var i = 0; i < length; ++i) {
+            var child = this.menu.children[i];
+            var level = this.availableLevels[i];
+            var won = hasBeaten(level, "hard");
+            if ((won || ((count ^ i) & 1)) && ((count ^ rand) & (1 << i))) {
+                var dyaw = won
+                    ? yuu.random.randsign(Math.PI / 2)
+                    : -Math.PI / 2;
+                targets.push(child.transform);
+                yaws.push(child.transform.yaw + dyaw);
+            }
+        }
+        if (targets.length) {
+            this.entity0.attach(new yuu.Animation(
+                ROTATE_ALL, { $s: targets, yaws: yaws }));
+        }
+        circleScene.clockTick(TICK_ROT2, HAND_TICK_BACK);
+        sounds[["tick", "tock"][count & 1]]
+            .createSound(yuu.audio, yuu.audio.currentTime, 0, 0.2, 1.0)
+            .connect(yuu.audio.music);
+    
+        return true;
+    },
+
+    init: function () {
+        circleScene.toBottom();
+        handScene.finished();
+        this._locks.increment("slam", "move");
+        this.entity0.animationQueue.enqueue(
+            MENU_APPEAR,
+            { $: this.entity0.transform })
+            .then(this._locks.decrementer("slam", "move"));
+    },
+
+    didWinLevel: function (level, difficulty, firstTime) {
+        var idx = this.availableLevels.indexOf(level);
+        circleScene.win();
+        if (firstTime)
+            this.entity0.animationQueue.enqueue(
+                FLASH, { $: this.menu.children[idx].data.flasher });
+        for (var i = idx; i < this.availableLevels.length; ++i) {
+            if (!hasBeaten(this.availableLevels[i], difficulty)) {
+                this._locks.increment("move");
+                this.changeActiveIndex(i, true)
+                    .then(this._locks.decrementer("move"));
+                
+                break;
+            }
+        }
+    },
+
+    changeActiveIndex: function (index, animate) {
+        var oldIndex = this.activeIndex;
+        var p;
+        this.activeIndex = index = yf.clamp(
+            index, 0, this.menu.children.length - 1);
+        if (index !== oldIndex && animate) {
+            this._locks.increment("slam");
+            var duration = Math.ceil(8 * Math.abs(oldIndex - index));
+            p = this.entity0.animationQueue.enqueue(
+                MENU_SLIDE, {
+                    $: this.menu.transform,
+                    x: 5 - 2 * index,
+                    duration: duration
+                });
+            p.then(this._locks.decrementer("slam"));
+        }
+        return p || Promise.resolve();
+    },
+    
+    left: yuu.cmd(function () {
+        if (!this._locks.some("move")) {
+            sounds[this.activeIndex === 0 ? "switchBroke" : "switch"].play();
+            handScene.movedLeft();
+            this.changeActiveIndex(this.activeIndex - 1, true);
+        }
+    }, "move the cursor left"),
+    right: yuu.cmd(function () {
+        if (!this._locks.some("move")) {
+            sounds[this.activeIndex === this.availableLevels.length - 1
+                   ? "switchBroke" : "switch"].play();
+            handScene.movedRight();
+            this.changeActiveIndex(this.activeIndex + 1, true);
+        }
+    }, "move the cursor right"),
+
+    slam: yuu.cmd(function () {
+        if (this._locks.some("slam"))
+            return;
+        var activeChild = this.menu.children[this.activeIndex];
+        this._locks.increment("slam", "move");
+        handScene.menuChoice();
+        circleScene.toBack();
+        circleScene.slam();
+        sounds.winding.play();
+        this.entity0.animationQueue.enqueue(
+            MENU_SLAM, {
+                cursor: this.pointer.transform,
+                select: activeChild.transform,
+                scene: this.entity0.transform,
+                mid: this.pointer.transform.y - 0.5,
+                line: this.pointer.transform.y - 1.5,
+                appear: activeChild.data.activate
+            }).then(function () {
+                this._locks.decrementer("slam", "move");
+                yuu.director.removeScene(this);
+            }.bind(this));
+    }, "choose the active menu item"),
+
+    inputs: {
+        resize: function () {
+            var base = new yuu.AABB(0, 0, 10, 10);
+            var vp = base.matchAspectRatio(yuu.viewport);
+            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
+        },
+
+        pinchout: function (p0, p1) {
+            p0 = this.layer0.worldFromDevice(p0);
+            p1 = this.layer0.worldFromDevice(p1);
+            if (vec2.sqrDist(p0, p1) > 1) {
+                this.slam();
+                return true;
+            }
+        },
+
+        hold: function (p) {
+            return this.inputs.dragstart.call(this, p);
+        },
+
+        release: function (p) {
+            if (this._dragStartX !== null)
+                return this.inputs.dragend.call(this, p);
+        },
+
+        dragstart: function (p) {
+            if (this._locks.some("move"))
+                return false;
+            p = this.layer0.worldFromDevice(p);
+            if (p.y > 6 && p.y < 8.5 && p.inside && this._dragStartX === null) {
+                sounds.switchOn.play();
+                this._locks.increment("move");
+                this._dragStartX = this.menu.transform.x;
+                return true;
+            }
+        },
+
+        dragdown: function (p0, p1) {
+            p0 = this.layer0.worldFromDevice(p0);
+            p1 = this.layer0.worldFromDevice(p1);
+
+            if (p0.x >= 4.5 && p0.x <= 5.5
+                && p0.y >= 6.0 && p0.y <= 8.5
+                && p0.y - p1.y > 1) {
+                this.slam();
+                return true;
+            }
+        },
+
+        drag: function (p0, p1) {
+            if (this._dragStartX !== null) {
+                p0 = this.layer0.worldFromDevice(p0);
+                p1 = this.layer0.worldFromDevice(p1);
+                this.menu.transform.x = this._dragStartX + (p1.x - p0.x);
+                var index = Math.round((5 - this.menu.transform.x) / 2);
+                this.changeActiveIndex(index);
+                return true;
+            }
+        },
+
+        dragend: function (p0, p1) {
+            if (this._dragStartX !== null) {
+                sounds.switchOff.play();
+                this._locks.decrement("move");
+                this._dragStartX = null;
+                var index = this.activeIndex;
+                this.activeIndex = (5 - this.menu.transform.x) / 2;
+                this.changeActiveIndex(index, true);
+                return true;
+            }
+        },
+
+        tap: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (p.y > 6 && p.y < 7 && p.inside) {
+                var dx = Math.round((p.x - 5) / 2);
+                if (dx === 0) this.slam();
+                else if (dx < 0) handScene.movedLeft();
+                else if (dx > 0) handScene.movedRight();
+                var idx = this.activeIndex;
+                this.changeActiveIndex(this.activeIndex + dx, true);
+                if (idx !== this.activeIndex)
+                    sounds.switch.play();
+                else
+                    sounds.switchBroke.play();
+                return true;
+            }
+            
+        },
+
+        doubletap: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (p.x >= 4.5 && p.x <= 5.5 && p.y >= 6.0 && p.y <= 8.5) {
+                this.slam();
+                return true;
+            }
+        },
+    },
+
+    resetEverything: yuu.cmd(function () {
+        storage.clear();
+        yuu.director.stop();
+        start();
+    }, "reset all saved data"),
+
+    unlock: yuu.cmd(function (d) {
+        LEVELS.forEach(function (level) { wonLevel(level, d); });
+        yuu.director.pushPopScene(new MenuScene());
+    }, "<difficulty>", "unlock all levels to the given difficulty"),
+
+    KEYBINDS: {
+        left: "left",
+        right: "right",
+        up: "right",
+        down: "left",
+        w: "right",
+        a: "left",
+        s: "left",
+        d: "right",
+        shift: "slam",
+        space: "slam",
+        z: "slam",
+        "`+r+e": "resetEverything",
+        "`+u+e": "unlock easy",
+        "`+u+h": "unlock hard",
+        gamepadbutton0: "slam",
+        gamepadbutton8: "help",
+        gamepadbutton9: "slam",
+        gamepadbutton13: "slam",
+        gamepadbutton14: "left",
+        gamepadbutton15: "right",
+    }
+});
+
+
+var BOOK_APPEAR = {
+    0: { set1:   { y: 1.5, x: -1.5 },
+         tween: { bgQuad: { alpha: 0.75 }, $: { y: 0, x: 0 }, },
+         duration: 30 }
+};
+
+var BOOK_DISMISS = {
+    0: { tween: { bgQuad: { alpha: 0 }, $: { y: 1.5, x: -1.5, } },
+         duration: 30 }
+};
+
+var KEYBOARD_PAGE = [0.25, 0.50, 0.50, 1.00];
+var POINTERS_PAGE = [0.25, 0.00, 0.50, 0.50];
+var GAMEPAD_PAGE = [0.00, 0.00, 0.25, 0.50];
+
+var BOOK_FORWARD = [
+    { 0:  { set:   { page2Quad: { color: [0.2, 0.2, 0.2, 1], texBounds: "page" } },
+            tween: { page1: { x: -1/3 / 2, scaleX: 0 },
+                     page2: { x: +1/3 / 2 },
+                     page2Quad: { color: [1, 1, 1, 1] },
+                   }, duration: 15, easing: "linear" },
+      15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
+                   page2Quad: { z: 0, texBounds: "page" } },
+            tween: { page1: { x: -1/3, scaleX: -2/3 },
+                     page2: { x: +1/3 }
+                   }, duration: 15, easing: "linear" },
+    },
+
+    { 0:  { tween: { page1: { x: -1/3 / 2 },
+                     page2: { x: +1/3 / 2, scaleX: 0 }
+                   }, duration: 15, easing: "linear" },
+      15: { set:   { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
+                     page2Quad: { z: 1, texBounds: [1.00, 0.5, 0.75, 1] } },
+            tween: { page1Quad: { color: [0.2, 0.2, 0.2, 1] },
+                     page1: { x: 0 },
+                     page2: { x: 0, scaleX: -2/3 },
+                   }, duration: 15, easing: "linear" },
+    },
+
+    BOOK_DISMISS
+];
+
+var BOOK_BACKWARD = [
+    { 0:   { tween: { page1: { x: -1/3 / 2, scaleX: 0 },
+                      page2: { x: +1/3 / 2 },
+                    }, duration: 15, easing: "linear" },
+      15: { set:   { page1Quad: { z: 1, texBounds: [0.50, 0.5, 0.75, 1] },
+                     page2Quad: { z: 0 } },
+            tween: { page2Quad: { color: [0.2, 0.2, 0.2, 1] },
+                     page1: { x: 0, scaleX: 2/3 },
+                     page2: { x: 0 },
+                   }, duration: 15, easing: "linear" },
+    },
+
+    { 0:  { set:   { page1Quad: { color: [0.2, 0.2, 0.2, 1] } },
+            tween: { page1Quad: { color: [1.0, 1.0, 1.0, 1] },
+                     page1: { x: -1/3 / 2 },
+                     page2: { x: +1/3 / 2, scaleX: 0 }
+                   }, duration: 15, easing: "linear" },
+    
+      15: { set:   { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
+                     page2Quad: { z: 0, texBounds: "page" } },
+            tween: { page1: { x: -1/3 },
+                     page2: { x: +1/3, scaleX: 2/3 },
+                   }, duration: 15, easing: "linear" },
+    },
+];
+
+BookScene = new yT(yuu.Scene, {
+    constructor: function () {
+        yuu.Scene.call(this);
+        var bg = new yuu.E(
+            new yuu.Transform().setScale([20, 20, 1]),
+            this.bgQuad = new yuu.QuadC()
+                .setColor([0, 0, 0, 0])
+                .setZ(-1));
+        this.page1 = new yuu.E(new yuu.Transform(),
+                               this.page1Quad = new yuu.QuadC(BOOK));
+        this.page1Quad.texBounds = [0.50, 0.5, 0.75, 1];
+        this.page1Quad.z = 1;
+        this.page2 = new yuu.E(new yuu.Transform(),
+                               this.page2Quad = new yuu.QuadC(BOOK));
+        this.page2Quad.texBounds = [0.25, 0.5, 0.50, 1];
+        this.page1.transform.scale = [2/3, 1, 1];
+        this.page2.transform.scale = [2/3, 1, 1];
+        this.entity0.attach(new yuu.Transform());
+        this.current = 0;
+        this._locks = new FlagSet("turn");
+        this.addEntities(bg, this.page1, this.page2);
+
+        this.dismissSound = new yuu.Instrument("@book-dismiss");
+        this.pageSounds = [new yuu.Instrument("@page-turn-1"),
+                           new yuu.Instrument("@page-turn-2"),
+                           new yuu.Instrument("@page-turn-3")];
+
+        this.ready = yuu.ready([this.dismissSound].concat(this.pageSounds));
+    },
+
+    help: yuu.cmd(function () {
+        this.skip();
+    }, "dismiss the help screen"),
+
+    licensing: yuu.cmd(function () {
+        var licensing = document.getElementById("yuu-licensing");
+        var parent = licensing.parentNode;
+        var spinner = document.createElement("div");
+        spinner.className = "yuu-spinner";
+        spinner.id = licensing.id;
+        parent.replaceChild(spinner, licensing);
+        Promise.all(
+            yf.map(yuu.GET,
+                   [yuu.PATH + "data/license.txt", "data/license.txt"]))
+            .then(function (texts) {
+                var text = texts.join("\n-- \n\n");
+                var p = document.createElement("pre");
+                p.textContent = text;
+                p.id = spinner.id;
+                parent.replaceChild(p, spinner);
+            });
+    }, "why would you ever want to run this?"),
+
+    init: function () {
+        this._anim(BOOK_APPEAR);
+        storage.setFlag("instructions");
+    },
+
+    _anim: function (anim) {
+        this._locks.increment("turn");
+        // FIXME: Need hooks from animations to audio
+        var completion = this._locks.decrementer("turn");
+        switch (anim) {
+        case BOOK_DISMISS:
+            this.dismissSound.play();
+            completion = yuu.director.removeScene.bind(yuu.director, this);
+            break;
+        case BOOK_APPEAR:
+            sounds.bookAppear.play();
+            break;
+        default:
+            yuu.random.choice(this.pageSounds).play();
+            break;
+        }
+
+        var device = yuu.director.preferredDevice();
+        this.entity0.attach(new yuu.Animation(
+            anim, {
+                $: this.entity0.transform,
+                page: device === "keyboard" ? KEYBOARD_PAGE
+                    : device === "gamepad" ? GAMEPAD_PAGE 
+                    : POINTERS_PAGE,
+                page1: this.page1.transform,
+                page2: this.page2.transform,
+                page1Quad: this.page1Quad,
+                page2Quad: this.page2Quad,
+                bgQuad: this.bgQuad
+            }, completion));
+    },
+
+    advance: yuu.cmd(function () {
+        if (this._locks.some("turn"))
+            return;
+        this._anim(BOOK_FORWARD[this.current++]);
+    }),
+
+    skip: yuu.cmd(function () {
+        if (this._locks.some("turn"))
+            return;
+        this._anim(BOOK_DISMISS);
+    }),
+
+    back: yuu.cmd(function () {
+        if (this._locks.some("turn"))
+            return;
+        if (this.current > 0)
+            this._anim(BOOK_BACKWARD[--this.current]);
+    }),
+
+    LOGOTYPE: new yuu.AABB(-0.16, -0.41, 0.12, -0.33),
+    COLOPHON: new yuu.AABB(-0.06, -0.41, 0.11, -0.28),
+
+    inputs: {
+        resize: function () {
+            var base = new yuu.AABB(-0.7, -0.55, 0.7, 0.55);
+            var vp = base.matchAspectRatio(yuu.viewport);
+            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
+        },
+
+        mousemove: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (this.current === BOOK_FORWARD.length - 1
+                && this.LOGOTYPE.contains(p)) {
+                this.cursor = "pointer";
+            } else if (this.current === 0 && this.COLOPHON.contains(p)) {
+                this.cursor = "pointer";
+            } else if (this.current === 0 || p.x >= -0.2) {
+                this.cursor = "";
+            } else {
+                this.cursor = "W-resize";
+            }
+        },
+        
+        tap: function (p) {
+            p = this.layer0.worldFromDevice(p);
+            if (this.current === BOOK_FORWARD.length - 1
+                && this.LOGOTYPE.contains(p)) {
+                yuu.openURL("http://www.yukkurigames.com/");
+            } else if (this.current === 0 && this.COLOPHON.contains(p)) {
+                yuu.director.showOverlay("colophon");
+            } else if (this.current === 0 || p.x >= -0.2) {
+                this.advance();
+            } else {
+                this.back();
+            }
+            return true;
+        },
+        swipeleft: function (event) { this.advance(); return true; },
+        swiperight: function (event) { this.back(); return true; },
+        dragleft: function (event) { this.advance(); return true; },
+        dragright: function (event) { this.back(); return true; },
+        swipeup: function (event) { this.skip(); return true; },
+        dragup: function (event) { this.skip(); return true; },
+
+        consume: yuu.Director.prototype.GESTURES
+            .concat(yuu.Director.prototype.CANVAS_EVENTS)
+    },
+
+    KEYBINDS: {
+        space: "advance",
+        shift: "advance",
+        z: "advance",
+        x: "advance",
+        right: "advance",
+        left: "back",
+        back: "skip",
+        escape: "skip",
+        gamepadbutton0: "advance",
+        gamepadbutton1: "skip",
+        gamepadbutton4: "back",
+        gamepadbutton5: "advance",
+        gamepadbutton8: "skip",
+        gamepadbutton9: "skip",
+        gamepadbutton14: "back",
+        gamepadbutton15: "advance",
+    }
+});
+
+var OUTER_FLIP_TICK = {
+    0: { tween1: { yaw: "yaw" }, duration: 15 }
+};
+
+var CIRCLE_TO_BOTTOM = {
+    0: { tween1: { pitch: Math.PI * 0.35, y: -0.3 }, duration: 35 }
+};
+
+var CIRCLE_TO_BACK = {
+    0: { tween1: { pitch: Math.PI * 0.15, y: -0.1 }, duration: 35 }
+};
+
+var CIRCLE_INNER_RATCHET = {
+    0:  { tween1: { rotation: "rotation1" }, duration: 15 },
+    10: { tween1: { rotation: "rotation2" }, duration: 10 },
+    20: { tween1: { rotation: "rotation1" }, duration: 20,
+          easing: yuu.Tween.STEPPED(5) },
+    40: { tween1: { rotation: "rotation2" }, duration: 15 }
+};
+
+var CIRCLE_INNER_WIND = {
+    0:  { tween1: { rotation: "rotation1" }, duration: 8 },
+    15: { tween1: { rotation: "rotation2" }, duration: 20 },
+};
+
+var BACKGROUND_DRIFT = {
+    0: [{ tween1: { yaw: Math.PI * 2 },
+          duration: 13 * 60 * 60, repeat: -Infinity, easing: "linear" },
+        { tween1: { scaleX: 0.5 },
+          duration: 11 * 60 * 60, repeat: -Infinity },
+        { tween1: { scaleY: 0.5 },
+          duration: 7 * 60 * 60, repeat: -Infinity }]
+};
+
+var HAND_TICK = {
+    0: { tween1: { rotation: "rotation" }, duration: 6 }
+};
+
+var CHIMES = [
+    // Nearly all derived from
+    // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
+    //
+    // All transposition & transcription errors are mine.
+
+    { name: "Westminster",
+      keys: ["D4", "E4"],
+      bars: ["0 2 1 -3",
+             "0 1 2 0",
+             "2 0 1 -3",
+             "2 1 0 -3",
+             "-3 1 2 0"]
+    },
+
+    { name: "Wittington",
+      keys: ["Eb4", "E4"],
+      bars: ["1 2 3 5 4 6 7 0",
+             "1 3 5 7 6 4 2 0",
+             "3 1 2 4 5 6 7 0",
+             "4 3 5 2 6 1 7 0",
+             "6 7 2 5 4 1 3 0",
+             "7 1 6 2 5 3 4 0",
+             "7 3 2 1 4 5 6 0",
+             "7 3 6 2 5 1 4 0",
+             "7 5 3 1 6 4 2 0",
+             "7 5 6 4 1 3 2 0",
+             "7 6 3 2 5 4 1 0",
+             "7 6 5 4 3 2 1 0"]
+    },
+
+    { name: "Canterbury",
+      keys: ["D4", "E4"],
+      bars: ["2 0 5 3 1 4",
+             "3 5 1 4 0 2",
+             "3 5 4 2 1 0",
+             "5 3 1 4 2 0",
+             "1 3 5 2 0 4",
+             "0 5 3 1 2 4",
+             "5 3 1 2 4 0"]
+    },
+
+    { name: "Trinity",
+      keys: ["F3", "D4"],
+      bars: ["5 4 3 2 1 0",
+             "2 4 3 1 2 0",
+             "5 3 4 2 1 0",
+             "4 3 2 1 5 2",
+             "5 0 4 3 2 1"]
+    },
+
+    /*
+    { name: "St. Michael's",
+      keys: ["F3", "C4"],
+      bars: ["7 6 5 4 3 2 1 0",
+             "7 1 2 3 6 4 5 0",
+             "4 3 2 5 1 6 7 0",
+             "6 7 2 3 1 4 5 0",
+             "4 6 2 7 3 1 5 0"]
+    },
+    */
+
+    { name: "Winchester",
+      keys: ["C4", "E4"],
+      bars: ["5 3 1 0 2 4",
+             "0 1 3 5 4 2",
+             "5 3 1 4 2 0",
+             "1 2 5 4 0 1",
+             "5 1 3 2 4 0"]
+    }
+             
+];
+
+function third (s) {
+    return "Q " + s.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
+}
+
+function silence (s) {
+    return "Q " + s.replace(/[^ ]+/g, "Z");
+}
+
+var TIMES1 = ["Q", "H", "H", "H", "H.", "H.", "W", "W"];
+var TIMES2 = ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
+              third, third, "Q.", "Q.",
+              "H", "H"];
+var TIMES3 = ["Q", "Q", silence, silence,
+              third, third, third, third,
+              "Q.", "Q.",
+              "H", "H"];
+
+function deck (pack, random) {
+    random = random || yuu.random;
+    var stock = [];
+    return function () {
+        if (stock.length === 0)
+            stock = random.shuffle(pack.slice());
+        return stock.pop();
+    };
+}
+
+function generateScore () {
+    var chimes = yuu.random.choice(CHIMES);
+    var bar = deck(chimes.bars);
+    function draw (t) {
+        return yf.isFunction(t) ? t(bar()) : t + " " + bar();
+    }
+    
+    function line (times) {
+        return yf.map(draw, yuu.random.shuffle(times)).join(" ");
+    }
+
+    var track = "{ - W HZ " + line(TIMES1)
+        + " { W HZ Z " + line(TIMES2)
+        + " { W HZ Z Z I Z " + line(TIMES3);
+    var key = yuu.random.choice(chimes.keys);
+    yuu.log("messages", "Playing " + chimes.name + " in " + key + " major.");
+    var score = yuu.parseScore(track, yuu.Scales.MAJOR, key);
+    score.key = key;
+    return score;
+}
+
+CircleScene = yT(yuu.Scene, {
+    constructor: function () {
+        yuu.Scene.call(this);
+        this.layer0.resize(-0.6, -0.6, 1.2, 1.2);
+        var arm = this.arm = new yuu.E(new yuu.Transform());
+        this.outer = new yuu.E(
+            new yuu.Transform([Math.sqrt(2) / 5, -Math.sqrt(2) / 5, 0]),
+            this.outerQuad = new yuu.QuadC(new yuu.Material("@circle-outer"))
+                .setZ(1)
+                .setLuminance(0.4)
+                .setSize([0.35417, 0.35417]));
+        arm.addChild(this.outer);
+
+        var rim = new yuu.E(
+            new yuu.Transform(),
+            this.rimQuad = new yuu.QuadC(new yuu.Material("@circle-rim"))
+                .setLuminance(0.2));
+        var inner = this.inner = new yuu.E(
+            new yuu.Transform(),
+            this.innerQuad = new yuu.QuadC(new yuu.Material("@circle-inner"))
+                .setLuminance(0.3));
+
+        var NOISY_QUADS = new yuu.ShaderProgram(
+            ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
+
+        var bgMat = new yuu.Material(
+            yuu.Texture.DEFAULT, NOISY_QUADS, { range: 0.8 });
+        bgMat.uniforms.cut = yf.volatile(cycler(100000));
+        var DIM = 16;
+        var batch = new yuu.QuadBatchC(DIM * DIM);
+        batch.material = bgMat;
+        var bg = new yuu.E(new yuu.Transform(), batch);
+        yf.irange(function (x) {
+            yf.irange(function (y) {
+                var quad = batch.createQuad();
+                quad.size = [1/4, 1/4];
+                quad.position = [(x - DIM / 2) * 1/4,
+                                 (y - DIM / 2) * 1/4];
+                quad.color = [0.12, 0.08, 0.16];
+                quad.texBounds = yf.repeat(x * DIM + y, 4);
+            }, DIM);
+        }, DIM);
+
+        this.entity0.addChild(bg);
+        this.entity0.attach(new yuu.Animation(
+            BACKGROUND_DRIFT, { $: bg.transform }));
+
+        this.ground = new yuu.E(new yuu.Transform());
+        this.ground.addChildren(rim, inner, arm);
+        this.entity0.addChild(this.ground);
+
+        this.music = yuu.audio.createGain();
+        this.music.gain.value = 0.3;
+        this.music.connect(yuu.audio.music);
+        this._finished = false;
+
+        this.ready = yuu.ready([
+            this.outerQuad.material,
+            this.innerQuad.material,
+            this.rimQuad.material,
+            bgMat.ready
+        ]);
+    },
+
+    help: yuu.cmd(function () {
+        yuu.director.pushScene(new BookScene());
+    }, "bring up the help screen"),
+
+    yuu: yuu.cmd(function () {
+        this.outerQuad.material = new yuu.Material("@circle-outer-ee");
+    }, "yuu~"),
+
+    KEYBINDS: {
+        slash: "help",
+        f1: "help",
+        gamepadbutton6: "help",
+        f10: "showOverlay preferences",
+        "shift+y+u+`": "yuu",
+        "gamepadbutton10+gamepadbutton11": "yuu",
+    },
+
+    inputs: {
+        resize: function () {
+            var vp = new yuu.AABB(-0.6, -0.6, 0.6, 0.6)
+                .matchAspectRatio(yuu.viewport);
+            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
+        }
+    },
+
+    toBottom: function () {
+        this.entity0.attach(
+            new yuu.Animation(CIRCLE_TO_BOTTOM, { $: this.ground.transform }));
+    },
+
+    wind: function () {
+        var rot1 = this.inner.transform.rotation;
+        quat.rotateZ(rot1, rot1, Math.PI / (2 * Math.E));
+        var rot2 = quat.rotateX(quat.create(), rot1, -Math.PI / 2);
+        quat.rotateY(rot2, rot2, Math.PI / 2);
+        quat.rotateX(rot2, rot2, -Math.PI / 2);
+        this.entity0.attach(
+            new yuu.Animation(CIRCLE_INNER_WIND, {
+                $: this.inner.transform,
+                rotation1: rot1,
+                rotation2: rot2
+            }));
+        this.tension = 0.5;
+        this.reversed = 0;
+        var score = [];
+        score.key = this.score && this.score.key;
+        this.score = score;
+    },
+
+    _musicSchedule: function (count) {
+        var t = yuu.director.currentAudioTime;
+        var note;
+
+        if (this._finished) {
+            if (this._finished === "won" && this.score.key) {
+                var score = yuu.parseScore(
+                    yuu.random.choice([
+                        "1 3 2 Z 0 { - 1 Z 2 Z 0",
+                        "1 2 3 Z 0 { - 1 Z 3 Z 0",
+                        "0 1 2 Z 4 { - 0 Z 2 Z 4",
+                    ]),
+                    yuu.Scales.MAJOR, this.score.key);
+                while ((note = score.shift())) {
+                    sounds.chime.createSound(
+                        yuu.audio,
+                        t + note.time / 4,
+                        note.hz,
+                        1.0, note.duration
+                    ).connect(this.music);
+                }
+            }
+            this._finished = false;
+            return false;
+        }
+
+        if (!(this.score && this.score.length)) {
+            this.score = generateScore();
+            this.playing = 0;
+        }
+
+        ++this.playing;
+        while (this.score.length && this.score[0].time < this.playing) {
+            note = this.score.shift();
+            sounds.chime.createSound(
+                yuu.audio,
+                t + note.time % 1 + yuu.random.gauss(0, 0.015),
+                note.hz,
+                1.0, note.duration
+            ).connect(this.music);
+        }
+
+        if ((this.tension *= 0.95) > 1) {
+            this.tension /= 2;
+            sounds.winding.createSound(yuu.audio, t, 0, 1.0, 1.0)
+                .connect(this.music);
+            var flip = !this.outer.transform.yaw * yuu.random.randsign(Math.PI);
+            this.entity0.attach(
+                new yuu.Animation(OUTER_FLIP_TICK, {
+                    $: this.outer.transform,
+                    yaw: flip
+                }));
+        } else {
+            [sounds.tick, sounds.tock][count & 1]
+                .createSound(yuu.audio, t, 0, 0.5, 1.0)
+                .connect(this.music);
+        }
+
+        this.clockTick(this.reversed-- > 0 ? TICK_REV : TICK_ROT);
+        
+        return true;
+    },
+
+    clockTick: function (amount, anim) {
+        var rot = this.arm.transform.rotation;
+        quat.multiply(rot, rot, amount || TICK_ROT);
+        this.arm.attach(new yuu.Animation(
+            anim || HAND_TICK,
+            { $: this.arm.transform, rotation: rot }));
+    },
+
+    toBack: function () {
+        this.wind();
+        this.entity0.attach(
+            new yuu.Animation(CIRCLE_TO_BACK, { $: this.ground.transform }));
+
+        this.playing = 4;
+        this.arm.attach(
+            new yuu.Ticker(this._musicSchedule.bind(this), 60));
+    },
+
+    win: function () {
+        this._finished = "won";
+        this.wind();
+        this.entity0.attach(
+            new yuu.Animation(FLASH, { $: this.innerQuad }),
+            new yuu.Animation(FLASH, { $: this.rimQuad }, null, 32),
+            new yuu.Animation(FLASH, { $: this.outerQuad }, null, 48)
+        );
+    },
+
+    lose: function () {
+        this._finished = "lose";
+        var rot1 = this.inner.transform.rotation;
+        quat.rotateZ(rot1, rot1, -Math.PI / Math.E);
+        var rot2 = quat.rotateZ(quat.create(), rot1, Math.PI / Math.E);
+        this.entity0.attach(
+            new yuu.Animation(CIRCLE_INNER_RATCHET, {
+                $: this.inner.transform,
+                rotation1: rot1,
+                rotation2: rot2
+            }));
+        sounds.regear.play();
+    },
+
+    rotated: function () {
+        this.tension += yuu.random.uniform(0.1);
+    },
+
+    slam: function () {
+        this.tension += yuu.random.uniform(0.2);
+    },
+
+    reverse: function () {
+        this.tension -= yuu.random.uniform(0.1);
+        this.reversed = Math.max(this.reversed, 0) + 1;
+    }
+
+});
diff --git a/src/yuu/audio.js b/src/yuu/audio.js
new file mode 100644 (file)
index 0000000..881924e
--- /dev/null
@@ -0,0 +1,524 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+
+    yuu.Audio = yT({
+        /** Audio context/source/buffer accessor
+
+            You probably don't need to make this yourself; one is made
+            named yuu.audio during initialization.
+
+            You can set the master volume with yuu.audio.masterVolume.
+        */
+        constructor: function () {
+            this._ctx = new window.AudioContext();
+            this._compressor = this._ctx.createDynamicsCompressor();
+            this._masterVolume = this._ctx.createGain();
+            this._masterVolume.connect(this._compressor);
+            this._compressor.connect(this._ctx.destination);
+            this._musicVolume = this._ctx.createGain();
+            this._musicVolume.connect(this._masterVolume);
+            this._masterVolume.gain.value = 0.5;
+            this._musicVolume.gain.value = 0.5;
+
+            this._bufferCache = {};
+            this._mute = false;
+            this._storage = null;
+            this._volume = this._masterVolume.gain.value;
+        },
+
+        destination: { alias: "_masterVolume", readonly: true },
+        music: { alias: "_musicVolume", readonly: true },
+
+        _readStorage: function () {
+            if (!this._storage)
+                return;
+            yf.each.call(this, function (prop) {
+                this[prop] = this._storage.getObject(prop, this[prop]);
+            }, ["volume", "musicVolume", "mute"]);
+        },
+
+        _writeStorage: yf.debounce(function () {
+            if (!this._storage)
+                return;
+            yf.each.call(this, function (prop) {
+                this._storage.setObject(prop, this[prop]);
+            }, ["volume", "musicVolume", "mute"]);
+        }),
+
+        storage: {
+            get: function () { return this._storage; },
+            set: function (v) {
+                this._storage = v;
+                this._readStorage();
+            }
+        },
+
+        mute: {
+            get: function () { return this._mute; },
+            set: function (v) {
+                this._mute = !!v;
+                this.volume = this.volume;
+            }
+        },
+
+        volume: {
+            get: function () { return this._volume; },
+            set: function (v) {
+                this._volume = v;
+                v = this._mute ? 0 : v;
+                this._masterVolume.gain.value = v;
+                this._writeStorage();
+            }
+        },
+
+        musicVolume: {
+            get: function () { return this._musicVolume.gain.value; },
+            set: function (v) {
+                this._musicVolume.gain.value = v;
+                this._writeStorage();
+            }
+        },
+
+        currentTime: { alias: "_ctx.currentTime" },
+
+        decodeAudioData: function (data) {
+            var ctx = this._ctx;
+            try {
+                return ctx.decodeAudioData(data);
+            } catch (exc) {
+                return new Promise(function (resolve, reject) {
+                    ctx.decodeAudioData(data, function (buffer) {
+                        resolve(buffer);
+                    }, function () {
+                        reject(new Error("Error decoding audio buffer"));
+                    });
+                });
+            }
+        },
+
+        createBufferSource: function (path) {
+            var source = this._ctx.createBufferSource();
+            var sample = new yuu.AudioSample(path, this);
+            if ((source.buffer = sample.buffer) === null) {
+                sample.ready.then(function () {
+                    source.buffer = sample.buffer;
+                });
+            }
+            return source;
+        },
+
+        sampleRate: { alias: "_ctx.sampleRate" },
+        createGain: { proxy: "_ctx.createGain" },
+        createOscillator: { proxy: "_ctx.createOscillator" },
+    });
+
+    // FIXME: This parsing is garbagey, would be better to parse when
+    // first handed a dfn and turn everything into a function.
+    function applyMod (s, v) {
+        if (yf.isFunction(s))
+            return s(v);
+        else if (s === +s)
+            return s;
+        else if (s[0] === "-" || s[0] === "+")
+            return v + (+s);
+        else if (s[0] === "x" || s[0] === "*")
+            return v * +s.substring(1);
+        else if (s[s.length - 1] === "%")
+            return v * (parseFloat(s) / 100);
+        else
+            return +s;
+    }
+
+    var Envelope = yuu.Envelope = yT({
+        constructor: yf.argcd(
+            function (pairs) {
+                Envelope.call(this, pairs, 1);
+            },
+            function (pairs, scale) {
+                pairs = pairs || { "0": 1, "100%": 1 };
+                this.ts = Object.keys(pairs);
+                this.vs = yf.map.call(pairs, yf.getter, this.ts);
+                this.scale = scale;
+                var a = 0, b = 0;
+                var unlimited = false;
+                yf.each(function (t) {
+                    if (+t) {
+                        a = Math.max(+t, a);
+                        b = Math.min(+t, b);
+                    }
+                    unlimited = unlimited || (t[t.length - 1] === "%");
+                }, this.ts);
+                this.minDuration = a - b;
+                this.maxDuration = (unlimited || a === b)
+                    ? Infinity
+                    : this.minDuration;
+                var vMin = Math.min.apply(Math, this.vs);
+                var vMax = Math.max.apply(Math, this.vs);
+                this.constant = vMin === vMax && this.vs[0] * this.scale;
+            }
+        ),
+
+        schedule: function (param, t0, scale, duration) {
+            if (this.constant !== false) {
+                param.setValueAtTime(scale * this.constant, t0);
+            } else {
+                yf.each.call(this, function (s, v) {
+                    v = v * scale * this.scale;
+                    var t = t0 + applyMod(s, duration);
+                    if (t === t0)
+                        param.setValueAtTime(v, t);
+                    else
+                        param.linearRampToValueAtTime(v, t);
+                }, this.ts, this.vs);
+            }
+        }
+    });
+
+    yuu.AudioSample = yuu.Caching(yT({
+        constructor: function (path, ctx) {
+            ctx = ctx || yuu.audio;
+            var url = yuu.resourcePath(path, "sound", "wav");
+            this.data = null;
+            this.ready = yuu.GET(url, { responseType: "arraybuffer" })
+                .then(ctx.decodeAudioData.bind(ctx))
+                .then(yf.setter.bind(this, "buffer"))
+                .then(yf.K(this));
+        }
+    }), function (args) { return args.length <= 2 ? args[0] : null; });
+
+    yuu.Modulator = yT({
+        constructor: function (dfn) {
+            this.envelope = new yuu.Envelope(dfn.envelope);
+            this.frequency = dfn.frequency;
+            this.index = dfn.index || 1.0;
+        },
+
+        createModulator: function (ctx, t0, fundamental, duration) {
+            var modulator = ctx.createOscillator();
+            modulator.frequency.value = applyMod(
+                this.frequency, fundamental);
+            modulator.start(t0);
+            modulator.stop(t0 + duration);
+            var modulatorG = ctx.createGain();
+            modulator.connect(modulatorG);
+            this.envelope.schedule(
+                modulatorG.gain, t0, this.index * fundamental, duration);
+            return modulatorG;
+        }
+    });
+
+    yuu.Instrument = yT({
+        constructor: function (dfn) {
+            if (yf.isString(dfn)) {
+                var sampleName = dfn;
+                dfn = { sample: {} };
+                dfn.sample[sampleName] = {};
+            }
+            this.envelope = new yuu.Envelope(dfn.envelope);
+            this.frequency = dfn.frequency || (dfn.sample ? {} : { "x1": 1.0 });
+            this.modulator = yf.map(
+                yf.new_(yuu.Modulator), yf.arrayify(dfn.modulator || []));
+            this.sample = dfn.sample || {};
+            this.ready = yuu.ready(
+                yf.map(yf.new_(yuu.AudioSample), Object.keys(this.sample)),
+                this);
+        },
+
+        createSound: function (ctx, t0, fundamental, amplitude, duration) {
+            // TODO: In the case of exactly one sample with a constant
+            // envelope, optimize out the extra gain node.
+            duration = yf.clamp(duration || 0,
+                                this.envelope.minDuration,
+                                this.envelope.maxDuration);
+            var ret = ctx.createGain();
+            var dst = ret;
+
+            yf.ipairs(function (name, params) {
+                var buffer = new yuu.AudioSample(name).buffer;
+                if (buffer && !params.loop)
+                    duration = Math.max(buffer.duration, duration);
+            }, this.sample);
+
+            var modulators = yf.map(function (modulator) {
+                return modulator.createModulator(
+                    ctx, t0, fundamental, duration);
+            }, this.modulator);
+
+            yf.ipairs.call(this, function (name, params) {
+                var src = ctx.createBufferSource(name);
+                src.loop = params.loop || false;
+                src.playbackRate.value = applyMod(
+                    params.playbackRate || 1, fundamental || ctx.sampleRate);
+                yf.each(function (mod) { mod.connect(src.playbackRate); },
+                        modulators);
+                if (params.duration)
+                    src.start(t0, params.offset || 0, params.duration);
+                else
+                    src.start(t0, params.offset || 0);
+                src.stop(t0 + duration);
+                src.connect(dst);
+            }, this.sample);
+
+            yf.ipairs.call(this, function (mfreq, mamp) {
+                var osc = ctx.createOscillator();
+                osc.frequency.value = applyMod(mfreq, fundamental);
+                osc.start(t0);
+                osc.stop(t0 + duration);
+                yf.each(function (mod) { mod.connect(osc.frequency); },
+                        modulators);
+                if (mamp !== 1) {
+                    var gain = ctx.createGain();
+                    gain.gain.value = mamp;
+                    osc.connect(gain);
+                    gain.connect(dst);
+                } else {
+                    osc.connect(dst);
+                }
+            }, this.frequency);
+
+            ret.gain.value = 0;
+            this.envelope.schedule(ret.gain, t0, amplitude, duration);
+            return ret;
+        },
+
+        play: yf.argcd(
+            function () {
+                return this.play(null, 0, 0, 1, 1);
+            },
+            function (ctx, t, freq, amp, duration) {
+                ctx = ctx || yuu.audio;
+                t = t || ctx.currentTime;
+                var g = this.createSound(ctx, t, freq, amp, duration);
+                g.connect(ctx.destination);
+                return g;
+            }
+        )
+    });
+
+    yuu.Instruments = yf.mapValues(yf.new_(yuu.Instrument), {
+        SINE: {
+            envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
+        },
+
+        ORGAN: {
+            envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
+            frequency: { "x1": 0.83, "x1.5": 0.17 }
+        },
+
+        SIREN: {
+            envelope: { "0": 1 },
+            modulator: {
+                envelope: { "0": 1 },
+                frequency: "1",
+                index: 0.2
+            }
+        },
+
+        BELL: {
+            envelope: { "0": 1, "2.5": 0.2, "5": 0 },
+            modulator: {
+                envelope: { "0": 1, "2.5": 0.2, "5": 0 },
+                frequency: "x1.5",
+            }
+        },
+
+        BRASS: {
+            envelope: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
+            modulator: {
+                envelope: { "0": 0, "0.2": 1, "0.4": 0.6,
+                            "-0.1": 0.5, "100%": 0 },
+                frequency: "x1",
+                index: 5.0
+            }
+        },
+    });
+
+    // Tune to A440 by default, although every interface should provide
+    // some ways to work around this. This gives C4 = ~261.63 Hz.
+    // https://en.wikipedia.org/wiki/Scientific_pitch_notation
+    yuu.C4_HZ = 440 * Math.pow(2, -9/12);
+
+    yuu.Scale = yT({
+        constructor: function (intervals) {
+            this.intervals = intervals;
+            this.length = this.intervals.length;
+            this.span = yf.foldl(function (a, b) { return a + b; }, intervals);
+        },
+
+        hz: function (tonic, degree, accidental) {
+            accidental = accidental || 0;
+            var s = this.span * ((degree / this.intervals.length) | 0)
+                + accidental;
+            degree %= this.intervals.length;
+            if (degree < 0) {
+                degree += this.intervals.length;
+                s -= this.span;
+            }
+            var i = 0;
+            while (degree >= 1) {
+                degree -= 1;
+                s += this.intervals[i];
+                i++;
+            }
+            if (degree > 0)
+                s += this.intervals[i] * degree;
+            return tonic * Math.pow(2, s / 1200.0);
+        }
+    });
+
+    yuu.Scales = yf.mapValues(yf.new_(yuu.Scale), {
+        CHROMATIC: yf.repeat(100, 12),
+        MINOR: [200, 100, 200, 200, 100, 200, 200],
+        MAJOR: [200, 200, 100, 200, 200, 200, 100],
+        WHOLE_TONE: [200, 200, 200, 200, 200, 200],
+        AUGMENTED: [300, 100, 300, 100, 300, 100],
+        _17ET: yf.repeat(1200 / 17, 17),
+        DOUBLE_HARMONIC: [100, 300, 100, 200, 100, 300, 100],
+    });
+
+    var DURATION = { T: 1/8, S: 1/4, I: 1/2, Q: 1, H: 2, W: 4,
+                     ".": 1.5, "/": 1/3, "<": -1 };
+    var ACCIDENTAL = { b: -1, "#": 1, t: 0.5, d: -0.5 };
+
+    var NOTE = /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
+
+    var LETTERS = { Z: null, X: null };
+
+    yuu.parseNote = function (note, scale, C4) {
+        return (C4 || yuu.C4_HZ) * Math.pow(2, LETTERS[note] / 12);
+    };
+
+    yuu.parseScore = function (score, scale, tonic, C4) {
+        // Note language:
+        //
+        // To play a scientific pitch note and advance the time, just
+        // use its name: G4, Cb2, A#0
+        //
+        // To adjust the length of the note, use T, S, I, Q (default),
+        // H, W for 32nd through whole. Append . to do
+        // time-and-a-half. Append / to cut into a third. Append < to
+        // go back in time.
+        //
+        // To play a note on the provided scale, use a 0-based number
+        // (which can be negative). To move the current scale up or
+        // down, use + or -. For example, in C major, 0 and C4 produce
+        // the same note; after a -, 0 and C3 produce the same note.
+        //
+        // To rest, use Z or X.
+        //
+        // To play multiple notes at the same time, enclose them all with
+        // < ... >. The time will advance in accordance with the shortest
+        // one.
+        //
+        // To reset the time, scale offset, and duration, use a {.
+        // This can be more convenient when writing pieces with
+        // multiple parts than grouping, e.g.
+        //     H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
+        // is easier to understand when split into multiple lines:
+        //       H 1 2 3 4
+        //     { H 8 7 6 5
+
+        scale = scale || yuu.Scales.MAJOR;
+        C4 = C4 || yuu.C4_HZ;
+        tonic = tonic || scale.tonic || C4;
+        if (yf.isString(tonic))
+            tonic = yuu.parseNote(tonic, C4);
+
+        var t = 0;
+        var notes = [];
+        var degree = 0;
+        var groupLength = 0;
+        var defaultDuration = "Q";
+        var match;
+
+        function calcDuration (d, m) { return d * DURATION[m]; }
+        function calcAccidental (d, m) { return d * ACCIDENTAL[m]; }
+
+        while ((match = NOTE.exec(score))) {
+            switch (match[5]) {
+            case "<":
+                groupLength = Infinity;
+                break;
+            case ">":
+                t += groupLength === Infinity ? 0 : groupLength;
+                groupLength = 0;
+                break;
+            case "+":
+                degree += scale.length;
+                break;
+            case "-":
+                degree -= scale.length;
+                break;
+            case "{":
+                t = 0;
+                degree = 0;
+                groupLength = 0;
+                defaultDuration = "Q";
+                break;
+            default:
+                if (match[5]) {
+                    defaultDuration = match[5];
+                    continue;
+                }
+                var letter = match[2];
+                var duration = yf.foldl(
+                    calcDuration, match[1] || defaultDuration, 1);
+                if (LETTERS[letter] !== null) {
+                    var offset = match[3];
+                    var accidental = yf.foldl(
+                        calcAccidental, match[4] || "", 0) * 100;
+                    notes.push({
+                        time: t,
+                        duration: duration,
+                        hz: letter
+                            ? C4 * Math.pow(2, LETTERS[letter]/12.0)
+                            : scale.hz(tonic, degree + (+offset || 0), accidental)
+                    });
+                }
+                if (groupLength && duration > 0)
+                    groupLength = Math.min(groupLength, duration);
+                else
+                    t += duration;
+            }
+        }
+
+        notes.sort(function (a, b) { return a.time - b.time; });
+        return notes;
+    };
+
+    yf.irange.call(LETTERS, function (i) {
+        yf.ipairs.call(this, function (l, o) {
+            var b = o + 12 * (i - 4);
+            this[l + i] = b;
+            yf.ipairs.call(this, function (s, m) {
+                this[l + s + i] = b + m;
+            }, ACCIDENTAL);
+        }, { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 });
+    }, 11);
+
+    yuu.registerInitHook(function () {
+        if (!window.AudioContext)
+            throw new Error("Web Audio isn't supported.");
+        yuu.audio = new yuu.Audio();
+        yuu.defaultCommands.volume = yuu.propcmd(
+            yuu.audio, "volume",
+            "get/set the current master audio volume", "0...1");
+        yuu.defaultCommands.musicVolume = yuu.propcmd(
+            yuu.audio, "musicVolume",
+            "get/set the current music volume", "0...1");
+        yuu.defaultCommands.mute = yuu.propcmd(
+            yuu.audio, "mute", "mute or unmute audio");
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/ce.js b/src/yuu/ce.js
new file mode 100644 (file)
index 0000000..bb5c6e5
--- /dev/null
@@ -0,0 +1,646 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    /** yuu-ce - entity/component system for the Yuu engine
+
+        Game logic in Yuu is implemented via entities and components.
+        Entities (yuu.E) represent "things" in the world, and
+        components (yuu.C) individual properties or abilities of those
+        things. By attaching and detaching components, entities gain
+        and lose those abilities.
+
+        This system prioritizes for convenience and simplicity over
+        performance. Many common optimizations in E/C systems, like
+        component pooling and array-of-structures, are not
+        implemented. (And are of questionable value in a language like
+        JavaScript.)
+    */
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+
+    yuu.E = yT({
+        constructor: function () {
+            /** Entity, a shell to customize with components
+
+                Entities exist as an aggregate of components (yuu.C).
+                They are used for components to talk to each other, or
+                other systems to handle systems in aggregate without
+                caring about the underlying details.
+
+                Entities expose components in two ways.
+                
+                First, components may have one or more slots; when a
+                component is attached to an entity it slots itself
+                into those properties on that entity. For example, if
+                you attach a Transform component to an entity, you can
+                retrieve it via e.transform.
+
+                Second, components may have one or more message
+                taps. This allows them to listen for, and respond to,
+                messages sent to the entity. Unlike slots many
+                attached components may have the same tap.
+            */
+            this.parent = null;
+            this.children = [];
+            this.taps = {};
+            this.attach.apply(this, arguments);
+        },
+
+        addChild: function (child) { this.addChildren(child); },
+        removeChild: function (child) { this.removeChildren(child); },
+
+        addChildren: function () {
+            yf.stash("parent", this, arguments);
+            this.children = this.children.concat(yf.slice(arguments));
+        },
+
+        removeChildren: function () {
+            this.children = yf.filter(
+                yf.lacks.bind(null, arguments), this.children);
+            yf.stash("parent", null, arguments);
+        },
+
+        attach: function () {
+            /** Attach a component to this entity.
+
+                If the entity already has a component in the same slots,
+                an error will be thrown.
+            */
+            for (var j = 0; j < arguments.length; ++j) {
+                var c = arguments[j];
+                var i;
+                for (i = 0; i < c.SLOTS.length; ++i)
+                    if (this[c.SLOTS[i].slot])
+                        throw new Error("Entity already has a " + c.SLOTS[i]);
+                for (i = 0; i < c.SLOTS.length; ++i)
+                    this[c.SLOTS[i]] = c;
+                for (i = 0; i < c.TAPS.length; ++i)
+                    this.taps[c.TAPS[i]] = (this.taps[c.TAPS[i]] || [])
+                        .concat(c);
+                c.entity = this;
+                c.attached(this);
+            }
+        },
+
+        detach: function () {
+            /** Detach a component from this entity */
+            for (var j = 0; j < arguments.length; ++j) {
+                var c = arguments[j];
+                var i;
+                for (i = 0; i < c.SLOTS.length; ++i)
+                    if (this[c.SLOTS[i].slot] !== c)
+                        throw new Error("Entity has a wrong " + c.SLOTS[i]);
+                for (i = 0; i < c.SLOTS.length; ++i)
+                    delete this[c.SLOTS[i]];
+                for (i = 0; i < c.TAPS.length; ++i)
+                    this.taps[c.TAPS[i]] = yf.without(this.taps[c.TAPS[i]], c);
+                c.entity = null;
+                c.detached(this);
+            }
+        },
+
+        _message: function (name, params) {
+            var taps = this.taps[name];
+            var children = this.children;
+            var i;
+            if (taps)
+                for (i = 0; i < taps.length; ++i)
+                    taps[i][name].apply(taps[i], params);
+            for (i = 0; i < children.length; ++i)
+                children[i]._message(name, params);
+        },
+        message: function (name) {
+            /** Message components listening on the named tap */
+            this._message(name, yf.tail(arguments));
+        },
+    });
+
+    yuu.C = yT({
+        entity: { value: null, writable: true },
+        SLOTS: { value: [], configurable: true },
+        TAPS: { value: [], configurable: true },
+
+        attached: function (entity) { },
+        detached: function (entity) { },
+    });
+
+    yuu.DataC = yT(yuu.C, {
+        /** A component for random scratch data
+
+            Storing this in a separate component rather than on the
+            entity directly reduces the chance of naming conflicts and
+            also the number of hidden classes.
+        */
+
+        constructor: function (data) {
+            Object.assign(this, data || {});
+        },
+
+        SLOTS: ["data"]
+    });
+
+
+    yuu.Animation = yT(yuu.C, {
+        constructor: function (timeline, params, completionHandler, delay) {
+            this.timeline = yf.mapValues(yf.arrayify, timeline);
+            this.params = params;
+            this.completionHandler = completionHandler;
+            this.keys = Object.keys(timeline)
+                .sort(function (a, b) {
+                    return +this._lookup(a) - +this._lookup(b);
+                }.bind(this));
+            this._t1 = +this._lookup(yf.last(this.keys)) + 1;
+            this._t = -(delay || 0);
+            this._pc = 0;
+            this._tweens = [];
+        },
+
+        attached: function () {
+            this.tick();
+        },
+
+        _lookup: function (k) {
+            return (k in this.params) ? this.params[k] : k;
+        },
+
+        set1: function (setter) {
+            var $ = this.params.$;
+            yf.ipairs.call(this, function (k, v) {
+                $[k] = this._lookup(v);
+            }, setter);
+        },
+
+        set: function (setters) {
+            yf.ipairs.call(this, function (name, setter) {
+                var $ = this._lookup(name);
+                yf.ipairs.call(this, function (k, v) {
+                    $[k] = this._lookup(v);
+                }, setter);
+            }, setters);
+        },
+
+        _addTween: function (tweens, instr) {
+            var repeat = instr.repeat || 0;
+            var cycles = Math.abs(repeat) + 1;
+            var easing = yf.isFunction(instr.easing)
+                ? instr.easing
+                : yuu.Tween[(instr.easing || "ease").toUpperCase()];
+            var duration, complete;
+
+            if ("complete" in instr) {
+                complete = this._lookup(instr.complete);
+                duration = (complete - this._t) / cycles;
+            } else if ("duration" in instr) {
+                duration = this._lookup(instr.duration);
+                complete = this._t + duration * cycles;
+            }
+
+            if (isFinite(cycles)) {
+                this._tweens.push(
+                    new yuu.Tween(tweens, duration, repeat, easing));
+                this._t1 = Math.max(complete + 1, this._t1);
+            } else {
+                this.entity.attach(new yuu.TweenC(
+                    tweens, duration, repeat, easing));
+            }
+        },
+
+        tween1: function (tween, instr) {
+            var nt = { $: this._lookup(instr.$) || this.params.$ };
+            yf.ipairs.call(this, function (k, v) {
+                nt[k] = [nt.$[k], this._lookup(v)];
+            }, tween);
+            this._addTween([nt], instr);
+        },
+
+        tween: function (targets, instr) {
+            var tweens = [];
+            yf.ipairs.call(this, function (name, tween) {
+                var nt = { $: this._lookup(name) || this.params.$ };
+                yf.ipairs.call(this, function (k, v) {
+                    nt[k] = [nt.$[k], this._lookup(v)];
+                }, tween);
+                tweens.push(nt);
+            }, targets);
+            this._addTween(tweens, instr);
+        },
+
+        tweenAll: function (tween, instr) {
+            var tweens = [];
+            var $s = this._lookup(instr.$s) || this.params.$s;
+            yf.irange.call(this, function (i) {
+                var nt = { $: $s[i] };
+                yf.ipairs.call(this, function (k, v) {
+                    nt[k] = [nt.$[k], this._lookup(v)[i]];
+                }, tween);
+                tweens.push(nt);
+            }, $s.length);
+            this._addTween(tweens, instr);
+        },
+
+        event: function (name) {
+            this.params[name](this, this.params);
+        },
+
+        _dispatch: function (instr) {
+            if (instr.set1)
+                this.set1(instr.set1);
+            if (instr.set)
+                this.set(instr.set);
+            if (instr.tween1)
+                this.tween1(instr.tween1, instr);
+            if (instr.tween)
+                this.tween(instr.tween, instr);
+            if (instr.tweenAll)
+                this.tweenAll(instr.tweenAll, instr);
+            if (instr.event)
+                this.event(instr.event);
+        },
+
+        tick: function () {
+            var t = this._t;
+            var i;
+            for (var key = this.keys[this._pc];
+                 this._lookup(key) <= t;
+                 key = this.keys[++this._pc]) {
+                yf.each.call(this, this._dispatch, this.timeline[key]);
+            }
+
+            for (i = this._tweens.length - 1; i >= 0; --i ) {
+                if (this._tweens[i].tick())
+                    this._tweens.splice(i, 1);
+            }
+
+            if (++this._t > this._t1) {
+                if (this.completionHandler)
+                    this.completionHandler(this);
+                this.entity.detach(this);
+            }
+        },
+
+        tock: function (p) {
+            for (var i = this._tweens.length - 1; i >= 0; --i)
+                this._tweens[i].tock(p);
+        },
+
+        TAPS: ["tick", "tock"]
+    });
+
+    yuu.Tween = yT({
+        /** Tween object properties over time
+
+            This component changes properties over time, and can
+            handle synchronizing multiple objects and multiple
+            properties.
+
+            The `property` is either a single object with the special
+            `$` property set to the object to tween and every other
+            property set to the properties to tween with values [min,
+            max], or a list of such objects. For example, to tween a.x
+            from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you
+            would pass
+
+                [{ $: a, x: [0, 1], y: [2, 3] },
+                 { $: b, z: [1, 2] }]
+
+            The `duration` is specified in ticks (e.g. calls to
+            director.tick).
+
+            `repeat` may be a positive number to repeat the tween that
+            many times, or a negative number to cycle back to the
+            minimum (and then back to the maximum, etc.) that many
+            times. `Infinity` will repeat the tween forever and
+            `-Infinity` will cycle the tween back and forth forever.
+
+            A custom easing equation may be provided. This is a
+            function which takes a p = [0, 1] and returns the eased p.
+        */
+
+        constructor: function (props, duration, repeat, easing) {
+            this._object = [];
+            this._property = [];
+            this._a = [];
+            this._b = [];
+            this._count = 0;
+            this.duration = duration || 60;
+            this.repeat = repeat || 0;
+            this.easing = easing || yuu.Tween.LINEAR;
+            yf.each.call(this, function (oab) {
+                yf.ipairs.call(this, function (name, ab) {
+                    if (name !== "$") {
+                        this._object.push(oab.$);
+                        this._property.push(name);
+                        this._a.push(ab[0]);
+                        this._b.push(ab[1]);
+                    }
+                }, oab);
+            }, yf.arrayify(props));
+            this._updateAt(0);
+        },
+
+        tick: function () {
+            var t = this._count / this.duration;
+            ++this._count;
+            if (t > 1 && !this.repeat)
+                return true;
+            else if (t >= 1 && this.repeat) {
+                if (this.repeat < 0) {
+                    var n = this._a;
+                    this._a = this._b;
+                    this._b = n;
+                }
+                this._count = 1;
+                t = 0;
+
+                if (this.repeat < 0) {
+                    this.repeat++;
+                } else if (this.repeat > 0) {
+                    this.repeat--;
+                }
+            }
+            this._updateAt(t);
+        },
+
+        tock: function (p) {
+            var t = (this._count + p - 1) / this.duration;
+            if (t <= 1)
+                this._updateAt(t);
+        },
+
+        _updateAt: function (t) {
+            var p = this.easing ? this.easing(t) : t;
+            for (var i = 0; i < this._object.length; ++i) {
+                // a was the existing property, b was the one provided
+                // by the user.  By lerping from b to a, the user can
+                // control the lerp type in some awkward cases -
+                // e.g. CSS DOM values are all exposed as strings so a
+                // will be a string/String, but if b is provided as a
+                // number/Number, this will lerp numerically.
+                //
+                // FIXME: This still doesn't work right if the lerp is
+                // later reversed due to negative repeats.
+                var object = this._object[i];
+                var property = this._property[i];
+                var a = this._a[i];
+                var b = this._b[i];
+                object[property] = yuu.lerp(b, a, 1 - p);
+            }
+        },
+
+        count: {
+            get: function () { return this._count; },
+            set: function (v) { this._count = Math.round(v); },
+        },
+
+        duration: { value: 60, chainable: true },
+        repeat: { value: 0, chainable: true },
+        easing: { value: null, chainable: true },
+    });
+
+    yuu.TweenC = yT(yuu.C, {
+        constructor: function () {
+            this._tween = yf.construct(yuu.Tween, arguments);
+        },
+
+        tick: function () {
+            if (this._tween.tick())
+                this.entity.detach();
+        },
+
+        tock: { proxy: "_tween.tock" },
+        count: { alias: "_tween.count" },
+        duration: { alias: "_tween.duration", chainable: true },
+        repeat: { alias: "_tween.repeat", chainable: true },
+        easing: { alias: "_tween.easing", chainable: true },
+
+        TAPS: ["tick", "tock"]
+    });
+
+    yuu.Tween.LINEAR = null;
+        /** No easing */
+
+    yuu.Tween.EASE = function (p) {
+        /** Ease in and out
+
+            This equation is from _Improving Noise_ (Perlin, 2002). It
+            is symmetrical around p=0.5 and has zero first and second
+            derivatives at p=0 and p=1.
+        */
+        return p * p * p * (p * (p * 6.0 - 15.0) + 10.0);
+    };
+
+    yuu.Tween.EASE_IN = function (p) {
+        return p * p * p;
+    };
+
+    yuu.Tween.METASPRING = function (amplitude, pulsation) {
+        /** A generator for springy tweens
+
+            The amplitude controls how far from the final position the
+            spring will bounce, as a multiple of the distance between the
+            start and end. A "normal" amplitude is around 0.5 to 1.5.
+
+            The pulsation constant controls the rigidity of the spring;
+            higher pulsation results in a spring that bounces more quickly
+            and more often during a fixed interval. A "normal" pulsation
+            constant is around 15 to 30.
+        */
+        return function (p) {
+            return 1 + Math.cos(pulsation * p + Math.PI) * (1 - p) * amplitude;
+        };
+    };
+
+    yuu.Tween.STEPPED = function (segments, alpha) {
+        return function (p) {
+            p = p * segments;
+            var lower = Math.floor(p);
+            var upper = Math.floor((p + alpha));
+            if (upper > lower) {
+                var p1 = 1 - (upper - p) / alpha;
+                return (lower + p1) / segments;
+            } else {
+                return lower / segments;
+            }
+        };
+    };
+
+    yuu.Transform = yT(yuu.C, {
+        /** A 3D position, rotation (as quaternion), and scale
+
+            This also serves as an object lesson for a simple slotted
+            component.
+        */
+        constructor: function (position, rotation, scale) {
+            this._position = vec3.clone(position || [0, 0, 0]);
+            this._rotation = quat.clone(rotation || [0, 0, 0, 1]);
+            this._scale = vec3.clone(scale || [1, 1, 1]);
+            this._matrix = mat4.create();
+            this._dirty = true;
+            this._version = 0;
+            this._parentVersion = null;
+        },
+
+        SLOTS: ["transform"],
+
+        position: {
+            chainable: true,
+            get: function () { return this._position.slice(); },
+            set: function (v) { this._dirty = true;
+                                vec3.copy(this._position, v); }
+        },
+        rotation: {
+            chainable: true,
+            get: function () { return this._rotation.slice(); },
+            set: function (v) { this._dirty = true;
+                                quat.normalize(this._rotation, v); }
+        },
+        scale: {
+            chainable: true,
+            get: function () { return this._scale.slice(); },
+            set: function (v) { this._dirty = true;
+                                vec3.copy(this._scale, v); }
+        },
+        x: {
+            chainable: true,
+            get: function () { return this._position[0]; },
+            set: function (x) { this._dirty = true; this._position[0] = x; }
+        },
+        y: {
+            chainable: true,
+            get: function () { return this._position[1]; },
+            set: function (x) { this._dirty = true; this._position[1] = x; }
+        },
+        z: {
+            chainable: true,
+            get: function () { return this._position[2]; },
+            set: function (x) { this._dirty = true; this._position[2] = x; }
+        },
+        xy: { swizzle: "xy", chainable: true },
+
+        scaleX: {
+            chainable: true,
+            get: function () { return this._scale[0]; },
+            set: function (x) { this._dirty = true; this._scale[0] = x; }
+        },
+        scaleY: {
+            chainable: true,
+            get: function () { return this._scale[1]; },
+            set: function (x) { this._dirty = true; this._scale[1] = x; }
+        },
+        scaleZ: {
+            chainable: true,
+            get: function () { return this._scale[2]; },
+            set: function (x) { this._dirty = true; this._scale[2] = x; }
+        },
+
+        worldToLocal: function (p) {
+            var x = (p.x || p[0] || 0);
+            var y = (p.y || p[1] || 0);
+            var z = (p.z || p[2] || 0);
+            var local = [x, y, z];
+            var matrix = mat4.clone(this.matrix);
+            return vec3.transformMat4(local, local, mat4.invert(matrix, matrix));
+        },
+
+        contains: function (p) {
+            p = this.worldToLocal(p);
+            return p[0] >= -0.5 && p[0] < 0.5
+                && p[1] >= -0.5 && p[1] < 0.5
+                && p[2] >= -0.5 && p[2] < 0.5;
+        },
+
+        ypr: {
+            chainable: true,
+            get: function () {
+                var q = this._rotation;
+                var x = q[0]; var sqx = x * x;
+                var y = q[1]; var sqy = y * y;
+                var z = q[2]; var sqz = z * z;
+                var w = q[3];
+                var abcd = w * x + y * z;
+                if (abcd > 0.499)
+                    return [2 * Math.atan2(x, w), Math.PI / 2, 0];
+                else if (abcd < -0.499)
+                    return [-2 * Math.atan2(x, w), -Math.PI / 2, 0];
+                else {
+                    var adbc = w * z - x * y;
+                    var acbd = w * y - x * z;
+                    return [Math.atan2(2 * adbc, 1 - 2 * (sqz + sqx)),
+                            Math.asin(2 * abcd),
+                            Math.atan2(2 * acbd, 1 - 2 * (sqy + sqx))];
+                }
+                
+            },
+            set: function (ypr) {
+                var q = this._rotation;
+                quat.identity(q);
+                quat.rotateZ(q, q, ypr[0]);
+                quat.rotateY(q, q, ypr[2]);
+                quat.rotateX(q, q, ypr[1]);
+                this._dirty = true;
+            }
+        },
+
+        yaw: { aliasSynthetic: "ypr[0]", chainable: true },
+        pitch: { aliasSynthetic: "ypr[1]", chainable: true },
+        roll: { aliasSynthetic: "ypr[2]", chainable: true },
+
+        matrix: {
+            get: function () {
+                var pt = this.entity.parent && this.entity.parent.transform;
+                var pm = pt && pt.matrix;
+                var ptVersion = pt && pt._version;
+                if (this._dirty || (ptVersion !== this._parentVersion)) {
+                    var m = this._matrix;
+                    mat4.identity(m);
+                    mat4.fromRotationTranslation(
+                        m, this._rotation, this._position);
+                    mat4.scale(m, m, this._scale);
+                    if (pm)
+                        mat4.multiply(m, pm, m);
+                    this._dirty = false;
+                    this._matrix = m;
+                    this._parentVersion = ptVersion;
+                    this._version = (this._version + 1) | 0;
+                }
+                return this._matrix;
+            }
+        }
+    });
+
+    yuu.Ticker = yT(yuu.C, {
+        /** Set a callback to run every n ticks
+
+            If the callback returns true, it is rescheduled for
+            execution (like setInterval). If it returns false, this
+            component is removed from the entity.
+        */
+        constructor: function (callback, interval, delay) {
+            this.callback = callback;
+            this.interval = interval;
+            this._accum = 0;
+            this._count = -(delay || 0);
+        },
+
+        tick: function () {
+            this._accum += 1;
+            if (this._accum === this.interval) {
+                this._accum = 0;
+                if (!this.callback(this._count++))
+                    this.entity.detach(this);
+            }
+        },
+
+        TAPS: ["tick"]
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/core.js b/src/yuu/core.js
new file mode 100644 (file)
index 0000000..8b16707
--- /dev/null
@@ -0,0 +1,896 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+
+    yuu.require = function (m) {
+        try { return require(m); }
+        catch (exc) { return null; }
+    };
+
+    if (!Math.sign)
+        require("./pre");
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+    var gui = yuu.require("nw.gui");
+    var fs = yuu.require("fs");
+    var stringLerp = this.stringLerp || yuu.require("string-lerp");
+
+    var initHooks = [];
+    var initOptions = null;
+
+    if (typeof document !== "undefined") {
+        var scripts = document.getElementsByTagName('script');
+        var path = yf.last(scripts).src.split('?')[0];
+        yuu.PATH = path.split('/').slice(0, -1).join('/') + '/';
+    } else {
+        yuu.PATH = "file://" + escape(module.__dirname) + "/";
+    }
+
+    yuu.registerInitHook = initHooks.push.bind(initHooks);
+        /** Register a hook to be called during Yuu initialization
+
+            Hooks are called in registration order with the module and
+            the options dictionary passed to the init method. (This is
+            also set to the module.)
+        */
+
+    function showError (exc, kind) {
+        var prefix = "yuu-" + (kind || "") + "error";
+        yuu.logError(exc);
+        var dialog = document.getElementById(prefix);
+        var errorMessage = document.getElementById(prefix + "-message");
+        if (errorMessage)
+            errorMessage.textContent = exc.message;
+        var errorStack = document.getElementById(prefix + "-stack");
+        if (errorStack)
+            errorStack.textContent = exc.message + "\n\n" + exc.stack;
+        return dialog;
+    }
+    yuu.showError = showError;
+
+    function fatalError (exc) {
+        var dialog = showError(exc, "fatal-");
+        if (dialog)
+            dialog.style.display = "block";
+        if (gui) {
+            gui.Window.get().show();
+            gui.Window.get().focus();
+        }
+        throw exc;
+    }
+
+    yuu.init = function (options) {
+        /** Initialize Yuu and call all registered hooks
+         */
+
+        if (gui) {
+            var win = gui.Window.get();
+            var nativeMenuBar = new gui.Menu({ type: "menubar" });
+            if (nativeMenuBar.createMacBuiltin) {
+                nativeMenuBar.createMacBuiltin(
+                    document.title, { hideEdit: true });
+                win.menu = nativeMenuBar;
+            }
+            var wkdoc = document;
+            win.on("minimize", function () {
+                var ev = new Event("visibilitychange");
+                wkdoc.hidden = true;
+                wkdoc.dispatchEvent(ev);
+            });
+            win.on("restore", function () {
+                var ev = new Event("visibilitychange");
+                wkdoc.hidden = false;
+                wkdoc.dispatchEvent(ev);
+            });
+        }
+
+        return new Promise(function (resolve) {
+            // TODO: Some kind of loading progress bar.
+            initOptions = options || {};
+            yuu.log("messages", "Initializing Yuu engine.");
+            var promises = [];
+            yf.each(function (hook) {
+                promises.push(hook.call(yuu, initOptions));
+            }, initHooks);
+            initHooks = null; // Bust future registerInitHook calls.
+            yuu.log("messages", "Initialization hooks complete.");
+            if (gui) {
+                gui.Window.get().show();
+                gui.Window.get().focus();
+            }
+            resolve(Promise.all(yf.filter(null, promises)));
+        }).then(function () {
+            yuu.log("messages", "Loading complete.");
+        }).catch(fatalError);
+    };
+
+    yuu.log = yf.argv(function (category, args) {
+        /** Log a message to the console.
+
+            This supports simple filtering by setting e.g.
+            `yuu.log.errors = true` to log anything with the
+            `"errors"` category.
+        */
+        if (!category || this.log[category]) {
+            switch (category) {
+            case "errors": return console.error.apply(console, args);
+            case "warnings": return console.warn.apply(console, args);
+            default: return console.log.apply(console, args);
+            }
+        }
+    });
+
+    yuu.log.errors = true;
+    yuu.log.warnings = true;
+    yuu.log.messages = true;
+
+    yuu.logError = function (e) {
+        yuu.log("errors", e.message || "unknown error", e);
+    };
+
+    yuu.GET = function (url, params) {
+        /** Promise the HTTP GET the contents of a URL. */
+        return new Promise(function (resolve, reject) {
+            var req = new XMLHttpRequest();
+            req.open("GET", url, true);
+            for (var k in params)
+                req[k] = params[k];
+            req.onload = function () {
+                var status = this.status;
+                // status === 0 is given by node-webkit for success.
+                if ((status >= 200 && status < 300) || status === 0)
+                    resolve(this.response);
+                else
+                    reject(new Error(
+                        url + ": " + status + ": " + this.statusText));
+            };
+            req.onabort = function () { reject(new Error("aborted")); };
+            req.onerror = function () { reject(new Error("network error")); };
+            req.ontimeout = function () { reject(new Error("timed out")); };
+            req.send(null);
+        });
+    };
+
+    yuu.Image = function (src) {
+        /** Promises a DOM Image. */
+        return new Promise(function (resolve, reject) {
+            var img = new Image();
+            img.onload = function () {
+                resolve(img);
+            };
+            img.onerror = function () {
+                var msg = "Unable to load " + img.src;
+                yuu.log("errors", msg);
+                reject(new Error(msg));
+            };
+            img.src = src;
+        });
+    };
+
+    /** Command parsing and execution
+
+        The command API serves several roles. It is a way to enable or
+        disable different game logic within different scenes; capture
+        and replay or automate game events; loosely or late-bind game
+        modules; customize input mappings; and a debugging tool to
+        help inspect or modify the state of a running program.
+
+        A command is a string of a command name followed by arguments
+        separated by whitespace. It's similar to a fully bound
+        closure. It is less flexible but easier to inspect, store,
+        replay, and pass around.
+
+        Command names are mapped to functions, grouped into sets, and
+        the sets pushed onto a stack. They are executed by passing the
+        command string to the execute function which walks the stack
+        looking for the matching command.
+
+        If the command string is prefaced with + or -, true or false
+        are appended to the argument list. e.g. `+command` is
+        equivalent to `command true` and `-command 1 b` is equivalent
+        to `command 1 b false`. By convention, commands of those forms
+        return their internal state when called with neither true or
+        false. This is useful for another special prefix, ++. When
+        called as `++command`, it is executed with no arguments, the
+        result inverted (with !), and then called again passing that
+        inverted value as the last argument.
+    */
+
+    function isCommand (f) {
+        return yf.isFunction(f) && f._isCommandFunction;
+    }
+
+    function cmdbind () {
+        // Commands are practically a subtype of functions. Binding
+        // them (which happens often, e.g. when Scenes register
+        // commands) should also return a command.
+        var f = Function.prototype.bind.apply(this, arguments);
+        // usage is still valid iff no new arguments were given.
+        return cmd(f, arguments.length <= 1 && this.usage, this.description);
+    }
+
+    var cmd = yuu.cmd = yf.argcd(
+        /** Decorate a function for command execution
+
+            Command functions need some special attributes to work
+            correctly. This decorator makes sure they have them.
+        */
+        function (f) { return yuu.cmd(f, null, null); },
+        function (f, description) { return yuu.cmd(f, null, description); },
+        function (f, usage, description) {
+            f._isCommandFunction = true;
+            f.usage = usage || " <value>".repeat(f.length).substring(1);
+            f.description = description || "no description provided";
+            f.bind = cmdbind;
+            return f;
+        }
+    );
+
+    yuu.propcmd = function (o, prop, desc, valspec) {
+        /** Generate a command function that controls a property
+
+            A common pattern for command functions is to simply get or
+            set a single object property. This wrapper will generate a
+            correct function to do that.
+        */
+        valspec = valspec || typeof o[prop];
+        desc = desc || "Retrieve or modify the value of " + prop;
+        return cmd(function () {
+            if (arguments.length)
+                o[prop] = arguments[0];
+            return o[prop];
+        }, "<" + valspec + "?>", desc);
+    };
+
+    var QUOTED_SPLIT = /[^"\s]+|"(?:\\"|[^"])+"/g;
+    var COMMAND_SPLIT = /\s+(&&|\|\||;)\s+/g;
+
+    function parseParam (param) {
+        if (yf.head(param) === "{" && yf.last(param) === "}")
+            return resolvePropertyPath(
+                this, param.substr(1, param.length - 2));
+        try { return JSON.parse(param); }
+        catch (exc) { return param; }
+    }
+
+    function parseCommand (cmdstring, ctx) {
+        /** Parse a command string into an invocation object.
+
+            The command string has a form like `+quux 1 2 3` or
+            `foobar "hello world"`.
+
+            Multiple commands can be joined in one string with &&, ||,
+            or ;. To use these characters literally as a command
+            argument place them in quotes.
+
+            Arguments wrapped in {}s are interpreted as property paths
+            for the provided context object. `{x[0].y}` will resolve
+            `ctx.x[0].y` and put that into the arguments array. To
+            avoid this behavior and get a literal string bounded by
+            {}, JSON-encode the string beforehand (e.g. `"{x[0].y}"`).
+
+            The returned array contains objects with three properties:
+            `name` - the command name to execute
+            `args` - an array of objects to pass as arguments
+            `toggle` - if the command value should be toggled ('++')
+                       and pushed into args
+            `cond` - "&&", "||", or ";", indicating what kind of
+                     conditional should be applied.
+        */
+
+        var invs = [];
+        var conds = cmdstring.split(COMMAND_SPLIT);
+        for (var i = -1; i < conds.length; i += 2) {
+            var args = conds[i + 1].match(QUOTED_SPLIT).map(parseParam, ctx);
+            var name = args.shift();
+            var toggle = false;
+            if (name[0] === "+" && name[1] === "+") {
+                name = name.substring(2);
+                toggle = true;
+            } else if (name[0] === "+") {
+                name = name.substring(1);
+                args.push(true);
+            } else if (name[0] === "-") {
+                name = name.substring(1);
+                args.push(false);
+            }
+            invs.push({ name: name, args: args, toggle: toggle,
+                        cond: conds[i] || ";"});
+        }
+        return invs;
+    }
+
+    yuu.CommandStack = yT({
+        constructor: function () {
+            /** A stack of command sets for command lookup and execution */
+            this._cmdsets = yf.slice(arguments);
+        },
+
+        push: function (cmdset) {
+            /** Add a command set to the lookup stack. */
+            this._cmdsets = this._cmdsets.concat(cmdset);
+        },
+
+        remove: function (cmdset) {
+            /** Remove a command set from the lookup stack. */
+            this._cmdsets = yf.without(this._cmdsets, cmdset);
+        },
+
+        insertBefore: function (cmdset, before) {
+            this._cmdsets = yf.insertBefore(
+                this._cmdsets.slice(), cmdset, before);
+        },
+
+        execute: function (cmdstring, ctx) {
+            /* Execute a command given a command string.
+
+               The command stack is searched top-down for the first
+               command with a matching name, and it is invoked.  No
+               other commands are called.
+
+               A command set may also provide a special function named
+               `$`. If no matching command name is found, this
+               function is called with the raw invocation object (the
+               result of yuu.parseCommand) and may return true to stop
+               processing as if the command had been found.
+            */
+            var invs = parseCommand(cmdstring, ctx);
+            var cond;
+            var res;
+            yf.each.call(this, function (inv) {
+                if ((inv.cond === "&&" && !cond) || (inv.cond === "||" && cond))
+                    return;
+                if (!yf.eachrUntil(function (cmdset) {
+                    var cmd = cmdset[inv.name];
+                    if (cmd) {
+                        if (inv.toggle)
+                            inv.args.push(!cmd.apply(null, inv.args));
+                        yuu.log("commands", "Executing:", inv.name,
+                                inv.args.map(JSON.stringify).join(" "));
+                        res = cmd.apply(null, inv.args);
+                        cond = res === undefined ? cond : !!res;
+                        yuu.log("commands", "Result:", JSON.stringify(res));
+                        return true;
+                    }
+                    return cmdset.$ && cmdset.$(inv);
+                }, this._cmdsets))
+                    yuu.log("errors", "Unknown command", inv.name);
+            }, invs);
+            return res;
+        }
+    });
+
+    yuu.extractCommands = function (object) {
+        var commands = {};
+        yf.each(function (prop) {
+            // Check the descriptor before checking the value, because
+            // checking the value of accessors (which should never be
+            // stable commands) is generally a bad idea during
+            // constructors, and command sets are often filled in during
+            // constructors.
+            if (yT.isDataDescriptor(yT.getPropertyDescriptor(object, prop))
+                && isCommand(object[prop]))
+                commands[prop] = object[prop].bind(object);
+        }, yf.allKeys(object));
+        return commands;
+    };
+
+    yuu.commandStack = new yuu.CommandStack(yuu.defaultCommands = {
+        /** The default command stack and set. */
+        cmds: yuu.cmd(function (term) {
+            term = term || "";
+            var cmds = [];
+            yuu.commandStack._cmdsets.forEach(function (cmdset) {
+                for (var cmdname in cmdset) {
+                    if (cmdname.indexOf(term) >= 0) {
+                        var cmd = cmdset[cmdname];
+                        var msg;
+                        if (cmd.usage)
+                            msg = [cmdname, cmd.usage, "--", cmd.description];
+                        else
+                            msg = [cmdname, "--", cmd.description];
+                        cmds.push(msg.join(" "));
+                    }
+                }
+            });
+            yuu.log("messages", cmds.join("\n"));
+        }, "<term?>", "display available commands (matching the term)"),
+
+        echo: yuu.cmd(function () {
+            yuu.log("messages", arguments);
+        }, "...", "echo arguments to the console"),
+        
+        log: yuu.cmd(function (name, state) {
+            if (state !== undefined)
+                yuu.log[name] = !!state;
+            return yuu.log[name];
+        }, "<category> <boolean?>", "enable/disable a logging category")
+
+    });
+
+    yuu.defaultCommands.showDevTools = yuu.cmd(function () {
+        if (gui)
+            gui.Window.get().showDevTools();
+    }, "show developer tools");
+
+    yuu.anchorPoint = function (anchor, x0, y0, x1, y1) {
+        /** Calculate the anchor point for a box given extents and anchor mode
+
+            This function is the inverse of yuu.bottomLeft.
+        */
+        switch (anchor) {
+        case "center": return [(x0 + x1) / 2, (y0 + y1) / 2];
+        case "top": return [(x0 + x1) / 2, y1];
+        case "bottom": return [(x0 + x1) / 2, y0];
+        case "left": return [x0, (y0 + y1) / 2];
+        case "right": return [x1, (y0 + y1) / 2];
+
+        case "topleft": return [x0, y1];
+        case "topright": return [x1, y1];
+        case "bottomleft": return [x0, y0];
+        case "bottomright": return [x0, y0];
+        default: return [anchor[0], anchor[1]];
+        }
+    };
+
+    yuu.bottomLeft = function (anchor, x, y, w, h) {
+        /** Calculate the bottom-left for a box given size and anchor mode
+
+            This function is the inverse of yuu.anchorPoint.
+        */
+        switch (anchor) {
+        case "center": return [x - w / 2, y - h / 2];
+        case "top": return [x - w / 2, y - h];
+        case "bottom": return [x - w / 2, y];
+        case "left": return [x, y - h / 2];
+        case "right": return [x - w, y - h / 2];
+
+        case "topleft": return [x, y - h];
+        case "topright": return [x - w, y - h];
+        case "bottomleft": return [x, y];
+        case "bottomright": return [x - w, y];
+        default: return [anchor[0], anchor[1]];
+        }
+    };
+
+    yuu.lerp = function (a, b, p) {
+        return (a !== null && a !== undefined && a.lerp)
+            ? a.lerp(b, p) : (b !== null && b !== undefined && b.lerp)
+            ? b.lerp(a, 1 - p) : p < 0.5 ? a : b;
+    };
+
+    yuu.bilerp = function (x0y0, x1y0, x0y1, x1y1, px, py) {
+        /** Bilinearly interpolate between four values in two dimensions */
+        return yuu.lerp(yuu.lerp(x0y0, x1y0, px), yuu.lerp(x0y1, x1y1, px), py);
+    };
+
+    function resolvePropertyPath (object, path) {
+        /** Look up a full property path
+
+            If a null is encountered in the path, this function returns
+            null. If undefined is encountered or a property is missing, it
+            returns undefined.
+        */
+        var parts = path.replace(/\[(\w+)\]/g, '.$1').split('.');
+        for (var i = 0;
+             i < parts.length && object !== undefined && object !== null;
+             ++i) {
+            object = object[parts[i]];
+        }
+        return object;
+    }
+
+    yuu.Random = yT({
+        /** Somewhat like Python's random.Random.
+
+            Passed a function that returns a uniform random variable in
+            [0, 1) it can do other useful randomization algorithms.
+
+            Its methods are implemented straightforwardly rather than
+            rigorously - this means they may not behave correctly in
+            common edge cases like precision loss.
+        */
+        constructor: function (generator) {
+            this.random = generator || Math.random;
+            this._spareGauss = null;
+        },
+
+        choice: function (seq) {
+            /** Return a random element from the provided array. */
+            return seq[this.randrange(0, seq.length)];
+        },
+
+        randrange: yf.argcd(
+            function (a) {
+                /** Return a uniform random integer in [0, a). */
+                return (this.random() * a) | 0;
+            },
+
+            function (a, b) {
+                /** Return a uniform random integer in [a, b). */
+                a = a | 0;
+                b = b | 0;
+                return a + ((this.random() * (b - a)) | 0);
+            },
+
+            function (a, b, step) {
+                /** Return a uniform random number in [a, b).
+
+                    The number is constrained to values of a + i * step
+                    where i is a non-negative integer.
+                */
+                var i = Math.ceil((b - a) / step);
+                return a + this.randrange(i) * step;
+            }
+        ),
+
+        uniform: yf.argcd(
+            function (a) {
+                /** Return a uniform random variable in [0, a). */
+                return a * this.random();
+            },
+            function (a, b) {
+                /** Return a uniform random variable in [a, b). */
+                return a + (b - a) * this.random();
+            }
+        ),
+
+        gauss: function (mean, sigma) {
+            var u = this._spareGauss, v, s;
+            this._spareGauss = null;
+
+            if (u === null) {
+                do {
+                    u = this.uniform(-1, 1);
+                    v = this.uniform(-1, 1);
+                    s = u * u + v * v;
+                } while (s >= 1.0 || s === 0.0);
+                var t = Math.sqrt(-2.0 * Math.log(s) / s);
+                this._spareGauss = v * t;
+                u *= t;
+            }
+            return mean + sigma * u;
+        },
+
+        randbool: yf.argcd(
+            /** Return true the given percent of the time (default 50%). */
+            function () { return this.random() < 0.5; },
+            function (a) { return this.random() < a; }
+        ),
+
+        randsign: function (v) {
+            return this.randbool() ? v : -v;
+        },
+
+        shuffle: function (seq) {
+            for (var i = seq.length - 1; i > 0; --i) {
+                var index = this.randrange(i + 1);
+                var temp = seq[i];
+                seq[i] = seq[index];
+                seq[index] = temp;
+            }
+            return seq;
+        },
+
+        discard: function (z) {
+            z = z | 0;
+            while (z-- > 0)
+                this.random();
+        }
+    });
+
+    yuu.createLCG = yf.argcd(
+        /** Linear congruential random generator
+
+            This returns a function that generates numbers [0, 1) as
+            with Math.random. You can also read or assign the `state`
+            attribute to set the internal state.
+        */
+        function () { return yuu.createLCG(Math.random() * 2147483647); },
+        function (seed) {
+            var state = seed | 0;
+            return function generator () {
+                state = (state * 1664525 + 1013904223) % 4294967296;
+                return state / 4294967296;
+            };
+        }
+    );
+
+    yuu.random = new yuu.Random();
+
+    function defaultKey (args) {
+        // Cache things that can be constructed with one string.
+        return args.length === 1 && yf.isString(args[0]) ? args[0] : null;
+    }
+
+    yuu.Caching = function (Type, cacheKey) {
+        function ctor () {
+            var k = ctor._cacheKey(arguments);
+            var o = k && ctor._cache[k];
+            if (!o)
+                o = ctor._cache[k] = yf.construct(ctor.Uncached, arguments);
+            return o;
+        }
+        ctor._cacheKey = cacheKey || defaultKey;
+        ctor._cache = {};
+        ctor.Uncached = Type;
+        return ctor;
+    };
+
+    yuu.transpose2d = function (a) {
+        for (var x = 0; x < a.length; ++x) {
+            for (var y = 0; y < x; ++y) {
+                var t = a[x][y];
+                a[x][y] = a[y][x];
+                a[y][x] = t;
+            }
+        }
+    };
+
+    yuu.normalizeRadians = function (theta) {
+        var PI = Math.PI;
+        return (theta + 3 * PI) % (2 * PI) - PI;
+    };
+
+    yuu.radians = function (v) {
+        return v * (Math.PI / 180.0);
+    };
+
+    yuu.degrees = function (v) {
+        return v * (180.0 / Math.PI);
+    };
+
+    var SHORT = /(\/|^)@(.+)$/;
+    yuu.resourcePath = function (path, category, ext) {
+        var match;
+        if ((match = path.match(SHORT))) {
+            path = path.replace(/^yuu\/@/, yuu.PATH + "@")
+                .replace(SHORT, "$1data/" + category + "/$2");
+            if (match[2].indexOf(".") === -1)
+                path += "." + ext;
+        }
+        return path;
+    };
+
+    yuu.ready = function (resources, result) {
+        return Promise.all(yf.filter(null, yf.pluck("ready", resources)))
+            .then(yf.K(result));
+    };
+
+    yuu.openURL = function (url) {
+        if (gui && gui.Shell)
+            gui.Shell.openExternal(url);
+        else
+            window.open(url);
+    };
+
+    function crossPlatformFilename (basename) {
+        return basename
+            // Replace D/M/Y with D-M-Y, and H:M:S with H.M.S.
+            .replace(/\//g, "-").replace(/:/g, ".")
+            // Replace all other problematic characters with _.
+            .replace(/["<>*?|\\]/g, "_");
+    }
+
+    yuu.downloadURL = function (url, suggestedName) {
+        var regex = /^data:[^;+]+;base64,(.*)$/;
+        var matches = url.match(regex);
+        suggestedName = crossPlatformFilename(suggestedName);
+        if (matches && fs) {
+            var data = matches[1];
+            var buffer = new Buffer(data, 'base64');
+            var HOME = process.env.HOME
+                || process.env.HOMEPATH
+                || process.env.USERPROFILE;
+            var filename = HOME + "/" + suggestedName;
+            console.log("Saving to", filename);
+            fs.writeFileSync(filename, buffer);
+        } else {
+            var link = document.createElement('a');
+            link.style.display = "none";
+            link.href = url;
+            link.download = suggestedName;
+            // Firefox (as of 28) won't download from a link not rooted in
+            // the document; so, root it and then remove it when done.
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+        }
+    };
+
+    yuu.AABB = yT({
+        constructor: yf.argcd(
+            function () { this.constructor(0, 0, 0, 0); },
+            function (w, h) { this.constructor(0, 0, w, h); },
+            function (x0, y0, x1, y1) {
+                this.x0 = x0;
+                this.y0 = y0;
+                this.x1 = x1;
+                this.y1 = y1;
+            }
+        ),
+
+        w: {
+            get: function () { return this.x1 - this.x0; },
+            set: function (w) { this.x1 = this.x0 + w; }
+        },
+
+        h: {
+            get: function () { return this.y1 - this.y0; },
+            set: function (h) { this.y1 = this.y0 + h; }
+        },
+
+        size: { swizzle: "wh" },
+
+        contains: yf.argcd(
+            function (p) { return this.contains(p.x, p.y); },
+            function (x, y) {
+                return x >= this.x0 && x < this.x1
+                    && y >= this.y0 && y < this.y1;
+            }
+        ),
+
+        matchAspectRatio: function (outer) {
+            var matched = new this.constructor(
+                this.x0, this.y0, this.x1, this.y1);
+            var aRatio = matched.w / matched.h;
+            var bRatio = outer.w / outer.h;
+            if (aRatio > bRatio) {
+                // too wide, must be taller
+                var h = matched.w / bRatio;
+                var dh = h - matched.h;
+                matched.y0 -= dh / 2;
+                matched.y1 += dh / 2;
+            } else {
+                // too tall, must be wider
+                var w = matched.h * bRatio;
+                var dw = w - matched.w;
+                matched.x0 -= dw / 2;
+                matched.x1 += dw / 2;
+            }
+            return matched;
+        },
+
+        alignedInside: function (outer, alignment) {
+            var x0, y0;
+            switch (alignment) {
+            case "bottomleft":
+                x0 = outer.x0;
+                y0 = outer.y0;
+                break;
+            case "bottom":
+                x0 = outer.x0 + (outer.w - this.w) / 2;
+                y0 = outer.y0;
+                break;
+            case "bottomright":
+                x0 = outer.x0 - this.w;
+                y0 = outer.y0;
+                break;
+            case "left":
+                x0 = outer.x0;
+                y0 = outer.x0 + (outer.h - this.h) / 2;
+                break;
+            case "center":
+                x0 = outer.x0 + (outer.w - this.w) / 2;
+                y0 = outer.x0 + (outer.h - this.h) / 2;
+                break;
+            case "right":
+                x0 = outer.x1 - this.w;
+                y0 = outer.x0 + (outer.h - this.h) / 2;
+                break;
+            case "topleft":
+                x0 = outer.x0;
+                y0 = outer.y1 - this.h;
+                break;
+            case "top":
+                x0 = outer.x0 + (outer.w - this.w) / 2;
+                y0 = outer.y1 - this.h;
+                break;
+            case "topright":
+                x0 = outer.x1 - this.w;
+                y0 = outer.y1 - this.h;
+                break;
+            }
+            return new this.constructor(x0, y0, x0 + this.w, y0 + this.h);
+        }
+    });
+
+    function splitPathExtension (path) {
+        var dot = path.lastIndexOf(".");
+        if (dot <= 0) return [path, ""];
+
+        var dir = path.lastIndexOf("/");
+        if (dot < dir) return [path, ""];
+
+        return [path.substring(0, dot), path.substring(dot)];
+    }
+    yuu.splitPathExtension = splitPathExtension;
+
+    if (stringLerp) {
+        yT.defineProperty(String.prototype, "lerp", function (b, p) {
+            b = b.toString();
+            // Never numericLerp - if that's desired force Numbers.
+            // Be more conservative than stringLerp since this runs
+            // often and the diff can't be easily hoisted.
+            return this.length * b.length > 256
+                ? stringLerp.fastLerp(this, b, p)
+                : stringLerp.diffLerp(this, b, p);
+        });
+    }
+
+    yT.defineProperties(Number.prototype, {
+        lerp: function (b, p) { return this + (b - this) * p; }
+    });
+
+    yT.defineProperties(Array.prototype, {
+        lerp: function (b, p) {
+            var length = Math.round(this.length.lerp(b.length, p));
+            var c = new this.constructor(length);
+            for (var i = 0; i < length; ++i) {
+                if (i >= this.length)
+                    c[i] = b[i];
+                else if (i >= b.length)
+                    c[i] = this[i];
+                else
+                    c[i] = this[i].lerp(b[i], p);
+            }
+            return c;
+        }
+    });
+
+    /** Typed array extensions
+
+        https://www.khronos.org/registry/typedarray/specs/1.0/
+        BUT: Read on for fun times in browser land~
+
+        Ideally we could just set these once on ArrayBufferView, but
+        the typed array specification doesn't require that such a
+        constructor actually exist. And in Firefox (18), it doesn't.
+        
+        More infurating, in Safari (7.0.3) Int8Array etc. are not
+        functions so this needs to be added to the prototype
+        directly. This is a violation of the specification which
+        requires such constructors, and ECMA which requires
+        constructors be functions, and common decency.
+    */
+
+    [ Float32Array, Float64Array, Int8Array, Uint8Array,
+      Int16Array, Uint16Array, Int32Array, Uint32Array
+    ].forEach(function (A) {
+        yT.defineProperties(A.prototype, {
+            slice: yf.argcd(
+                /** Like Array's slice, but for typed arrays */
+                function () { return new this.constructor(this); },
+                function (begin) {
+                    return new this.constructor(this.subarray(begin));
+                },
+                function (begin, end) {
+                    return new this.constructor(this.subarray(begin, end));
+                }
+            ),
+
+            fill: Array.prototype.fill,
+            reverse: Array.prototype.reverse,
+
+            lerp: function (b, p) {
+                if (p === 0)
+                    return this.slice();
+                else if (p === 1)
+                    return b.slice();
+                var c = new this.constructor(this.length);
+                for (var i = 0; i < this.length; ++i)
+                    c[i] = this[i] + (b[i] - this[i]) * p;
+                return c;
+            }
+        });
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined" ? (this.yuu = {}) : exports);
diff --git a/src/yuu/data/license.txt b/src/yuu/data/license.txt
new file mode 100644 (file)
index 0000000..6c2b4e7
--- /dev/null
@@ -0,0 +1,507 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
+-- 
+
+glMatrix - http://glmatrix.net/
+
+Copyright 2013 Brandon Jones, Colin MacKenzie IV
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must
+     not claim that you wrote the original software. If you use this
+     software in a product, an acknowledgment in the product
+     documentation would be appreciated but is not required.
+
+  2. Altered source versions must be plainly marked as such, and must
+     not be misrepresented as being the original software.
+
+  3. This notice may not be removed or altered from any source
+     distribution.
+
+-- 
+     
+Hammer.js - http://eightmedia.github.io/hammer.js/
+
+Copyright 2011-2014 by Jorik Tangelder (Eight Media)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-- 
+
+Fonts
+
+Copyright (c) 2014, Mozilla Foundation https://mozilla.org/
+Copyright (c) 2014, Telefonica S.A.
+with Reserved Font Name Fira Sans.
+
+Copyright (c) 2014, Mozilla Foundation https://mozilla.org/
+Copyright (c) 2014, Telefonica S.A.
+with Reserved Font Name Fira Mono.
+
+Copyright (c) 2014, Dave Gandy http://fontawesome.io/
+with Reserved Font Name Font Awesome
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+
+-- 
+
+This program may be distributed along with node-webkit, a wrapper for
+packaging web applications for standalone use. If so, the accompanying
+`node-webkit credits.html' contains its licensing information.
+
+This program is not a derivative work of node-webkit but rather "mere
+aggregation." You do not need to account for node-webkit's licensing
+terms to modify and/or redistribute parts of this program unless you
+also modify and/or redistribute node-webkit.
diff --git a/src/yuu/data/shaders/color.frag b/src/yuu/data/shaders/color.frag
new file mode 100644 (file)
index 0000000..3d6f67e
--- /dev/null
@@ -0,0 +1,12 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+precision mediump float;
+
+varying vec4 fColor;
+
+void main(void) {
+    gl_FragColor = fColor;
+}
diff --git a/src/yuu/data/shaders/default.frag b/src/yuu/data/shaders/default.frag
new file mode 100644 (file)
index 0000000..712aa80
--- /dev/null
@@ -0,0 +1,15 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+precision mediump float;
+
+varying vec2 fTexCoord;
+varying vec4 fColor;
+uniform sampler2D tex;
+
+void main(void) {
+    gl_FragColor = vec4(fColor.rgb * fColor.a, fColor.a)
+        * texture2D(tex, fTexCoord);
+}
diff --git a/src/yuu/data/shaders/default.vert b/src/yuu/data/shaders/default.vert
new file mode 100644 (file)
index 0000000..5a21e7d
--- /dev/null
@@ -0,0 +1,22 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+precision mediump float;
+
+attribute vec3 position;
+attribute vec2 texCoord;
+attribute vec4 color;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 projection;
+varying vec2 fTexCoord;
+varying vec4 fColor;
+
+void main(void) {
+    gl_Position = projection * view * model * vec4(position, 1.0);
+    fTexCoord = texCoord;
+    fColor = color;
+}
diff --git a/src/yuu/data/yuu.css b/src/yuu/data/yuu.css
new file mode 100644 (file)
index 0000000..b572735
--- /dev/null
@@ -0,0 +1,364 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+@font-face {
+  font-family: 'FontAwesome';
+  src: url('../../ext/font-awesome.woff') format('woff');
+  font-weight: normal;
+  font-style: normal;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-UltraLight.woff');
+    font-weight: 200;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-UltraLightItalic.woff');
+    font-weight: 200;
+    font-style: italic;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-Regular.woff');
+    font-weight: 400;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-Italic.woff');
+    font-weight: 400;
+    font-style: italic;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-Bold.woff');
+    font-weight: 700;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: 'Fira Sans';
+    src: url('../../ext/FiraSans-BoldItalic.woff');
+    font-weight: 700;
+    font-style: italic;
+}
+
+@font-face {
+    font-family: 'Fira Mono';
+    src: url('../../ext/FiraMono-Regular.woff');
+    font-weight: 400;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: 'Fira Mono';
+    src: url('../../ext/FiraMono-Bold.woff');
+    font-weight: 700;
+    font-style: normal;
+}
+
+pre, tt, code, kbd {
+    font-family: 'Fira Mono', FontAwesome, monospace;
+}
+
+body {
+    overflow: hidden;
+    margin: 0;
+    padding: 0;
+    font-family: 'Fira Sans', FontAwesome, sans-serif;
+}
+
+#yuu-canvas {
+    /* Specifying only width/height gives incorrect results on Chrome
+       33.0.1750.152 when fullscreen on > 1 devicePixelRatio. The
+       canvas takes on the correct size, but is centered in a page of
+       e.g. 2x for 2 DPR so you only see the top-left quadrant.
+
+       Specifying only top/bottom/left/right 0 also breaks, because in the
+       absence of a CSS size the browser tries to set the client size to
+       the canvas buffer size, which means it grows/shrinks by the DPR
+       every resize event.
+       
+       Specifying all six attributes makes it work as desired. */
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0;
+}
+
+#yuu-canvas:focus {
+    outline: inherit;
+}
+
+#yuu-licensing {
+    padding-left: 2em;
+    padding-right: 2em;
+    text-align: center;
+    font-size: 0.7em;
+}
+
+pre#yuu-licensing {
+    text-align: left;
+}
+
+[data-yuu-command] {
+    cursor: pointer;
+}
+
+/* Animations */
+
+.yuu-from-top-right {
+    transform: translate(50vw, -110%) !important;
+    -webkit-transform: translate(50vw, -110%) !important;
+}
+
+.yuu-from-top {
+    transform: translate(-50%, -110%) !important;
+    -webkit-transform: translate(-50%, -110%) !important;
+}
+
+.yuu-fade {
+    opacity: 0 !important;
+}
+
+.yuu-squish {
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+    max-width: 0 !important;
+    min-width: 0 !important;
+    overflow: hidden !important;
+    padding-left: 0em !important;
+    padding-right: 0em !important;
+}
+
+/* Toasts are short-lived feedback to user actions. */
+.yuu-toast {
+    pointer-events: none;
+    background-color: rgba(50, 50, 50, 0.5);
+    border-radius: 0.2em;
+    border: solid rgba(255, 255, 255, 0.5) 1px;
+    color: #eee;
+    display: table-cell;
+    float: right;
+    font-size: 3em;
+    margin-left: 0.125em;
+    margin-right: 0.125em;
+    margin-top: 0.25em;
+    max-width: 60%;
+    /* Minimum size is a square: 1.25 + 0.125 * 2 for padding = 1.5em,
+       same as the line height. */
+    min-width: 1.4em;
+    padding: 0 0.125em;
+    padding-top: 0.15em;
+    position: relative;
+    text-align: center;
+    transition: all 0.5s;
+    -webkit-transition: all 0.5s;
+}
+
+/* Overlays are hidden HTML-based scenes that the director can load.
+   These appear over the game, and are modal. The primary use case is
+   configuration menus, copyright information, error feedback,
+   etc. */
+
+.yuu-overlay {
+    background-color: rgba(50, 50, 50, 0.9);
+    border-radius: 0.2em;
+    border: solid rgba(255, 255, 255, 0.9) 1px;
+    color: #eee;
+    display: none;
+    width: 60%;
+    max-width: 600px;
+    min-width: 300px;
+    margin-left: auto;
+    margin-right: auto;
+    left: 50%;
+    max-height: 80%;
+    overflow: auto;
+    padding: 0 1em 1em 1em;
+    position: fixed;
+    transform: translate(-50%, 10vh) scale(1, 1);
+    -webkit-transform: translate(-50%, 10vh) scale(1, 1);
+    transition: transform 0.3s, opacity 0.3s;
+    -webkit-transition: -webkit-transform 0.3s, opacity 0.3s;
+}
+
+/* Overlays are focusable but should not show it - they are always
+   somewhere in the event tree when visible. */
+.yuu-overlay:focus {
+    outline: inherit;
+}
+
+.yuu-overlay h1 {
+    font-size: 1.2em;
+    font-weight: normal;
+    text-align: center;
+}
+
+.yuu-overlay h2 {
+    font-size: 1.1em;
+    font-weight: normal;
+}
+
+.yuu-overlay hr {
+    margin-bottom: 1em;
+    margin-top: 0.5em;
+}
+
+/* For consistency overlays use custom CSS for controls, which
+   means we need a default focused behavior. */
+.yuu-overlay *:focus {
+    outline: solid grey 1px;
+}
+
+div[data-yuu-command=dismiss] {
+    font-size: 1.5em;
+    width: 1.25em;
+    height: 1.25em;
+    text-align: center;
+    position: fixed;
+    margin-left: -0.6667em;
+}
+
+div[data-yuu-command=dismiss]:after {
+    content: "\f00d";
+}
+
+/* Table layout for options screens. In general, two or three columns,
+   the leftmost is a simple control, the middle/last is a label, and
+   the last is a more complicated control like a range or select
+   dropdown. */
+
+.yuu-options {
+    border-collapse: separate;
+    border-spacing: 0.25em;
+}
+
+.yuu-options td:first-child {
+    min-width: 2em;
+    white-space: nowrap;
+}
+
+.yuu-options td:last-child {
+    width: 100%;
+}
+
+/* De/re-style checkboxes. This means hiding the actual
+   checkbox and making it tiny, and instead filling in the
+   label immediately after it. */
+input[type=checkbox][data-yuu-command] {
+    max-width: 0;
+    opacity: 0;
+}
+
+input[type=checkbox][data-yuu-command] + label[for] {
+    cursor: pointer;
+    display: inline-block;
+    text-align: center;
+    width: 1.3333em;
+    font-size: 1.25em;
+}
+
+input[type=checkbox][data-yuu-command] + label[for]:before {
+    display: inline-block;
+    padding-top: 0.2em;
+}
+
+input[type=checkbox][data-yuu-command] + label[for]:before {
+    content: "\f096";
+}
+
+input[type=checkbox][data-yuu-command]:checked + label[for]:before {
+    content: "\f046";
+}
+
+input[type=checkbox][data-yuu-command]:focus + label[for] {
+    outline: solid grey 1px;
+}
+
+/* De/re-style ranges. */
+input[type=range][data-yuu-command] {
+    -webkit-appearance: none;
+    background-color: gray;
+}
+
+input[type=range][data-yuu-command]::-moz-range-track {
+    background: gray;
+    border: none;
+    outline: none;
+}
+
+input[type=range][data-yuu-command]::-webkit-slider-thumb {
+    -webkit-appearance: none;
+    background-color: #444;
+    width: 1.5em;
+    height: 1em;
+}
+
+input[type=range][data-yuu-command]::-moz-range-thumb {
+    border: none;
+    background-color: #444;
+    width: 1.5em;
+    height: 1em;
+}
+
+/* Special-case icons for the mute checkbox. */
+
+input[type=checkbox][data-yuu-command=mute]:checked + label[for]:before {
+    content: "\f026";
+}
+
+input[type=checkbox][data-yuu-command=mute] + label[for]:before {
+    content: "\f028";
+}
+
+@-moz-keyframes spin {
+    from { -moz-transform: rotate(0deg); }
+    to { -moz-transform: rotate(360deg); }
+}
+
+@-webkit-keyframes spin {
+    from { -webkit-transform: rotate(0deg); }
+    to { -webkit-transform: rotate(360deg); }
+}
+
+@keyframes spin {
+   from { transform: rotate(0deg); }
+   to { transform: rotate(360deg); }
+}
+
+.yuu-spinner:after {
+    content: "â—”";
+    -webkit-animation: spin 1s linear infinite;
+    -moz-animation: spin 1s linear infinite;
+    animation: spin 1s linear infinite;
+    display: inline-block;
+}
+
+dl {
+    text-align: center;
+}
+
+dt {
+    margin-top: 1em;
+    margin-bottom: 0.25em;
+    font-size: 0.8em;
+    font-weight: 200;
+}
+
+dd {
+    margin-left: 0;
+}
diff --git a/src/yuu/director.js b/src/yuu/director.js
new file mode 100644 (file)
index 0000000..8723014
--- /dev/null
@@ -0,0 +1,696 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+
+    // It's vaguely plausible to want a director without any scenes
+    // (only entity0 and the canvas), which means the renderer is not
+    // required.
+    if (!yuu.E) require("./ce");
+    if (!yuu.InputState) require("./input");
+    if (!yuu.Material) require("./gfx");
+
+    yuu.Director = yT({
+        constructor: function (commandStack, input, tickHz) {
+            /** Manage and update a set of Scenes
+
+                The director is responsible for calling two functions
+                regularly on the Scene instances it controls, `tick` and
+                `render`. `tick` is called at a regular interval (or at
+                least pretends to be called at one, browser scheduler
+                permitting), and `render` is called when the browser asks
+                for a new display frame.
+            */
+            this._scenes = [];
+            this.entity0 = new yuu.E();
+            this._commandStack = commandStack || yuu.commandStack;
+            this.input = input || new yuu.InputState([yuu.defaultKeybinds]);
+            this._events = {};
+            this._tickCount = 0;
+            this._timerStart = 0;
+            this._audioOffset = 0;
+            this._rafId = null;
+            this._tickHz = tickHz || 60;
+            this._afterRender = [];
+
+            this.commands = yuu.extractCommands(this);
+            this._commandStack.push(this.commands);
+            this._dogesture = this.__dogesture.bind(this);
+            this._gesture = null;
+            this._resized = false;
+            this._toasts = {};
+            this._devices = {};
+        },
+
+        pushScene: function (scene) {
+            /** Add a Scene onto the director's stack */
+            this.insertScene(scene, this._scenes.length);
+        },
+
+        popScene: function () {
+            /** Remove the top scene from the director's stack */
+            this.removeScene(yf.last(this._scenes));
+        },
+
+        pushPopScene: function (scene) {
+            /** Replace the top scene on the stack */
+            this.popScene();
+            this.pushScene(scene);
+        },
+
+        insertScene: function (scene, idx) {
+            var scenes = this._scenes.slice();
+            scenes.splice(idx, 0, scene);
+            this._scenes = scenes;
+            this._commandStack.insertBefore(
+                scene.commands,
+                this._scenes[idx + 1] && this._scenes[idx + 1].commands);
+            this.input.insertBefore(
+                scene.keybinds,
+                this._scenes[idx + 1] && this._scenes[idx + 1].keybinds);
+            scene.init(this);
+            if (scene.inputs.resize)
+                scene.inputs.resize.call(scene, yuu.canvas);
+        },
+
+        insertUnderScene: function (scene, over) {
+            return this.insertScene(scene, this._scenes.indexOf(over));
+        },
+
+        removeScene: function (scene) {
+            /** Remove a Scene onto the director's stack */
+            this._scenes = yf.without(this._scenes, scene);
+            scene.done();
+            this.input.remove(scene.keybinds);
+            this._commandStack.remove(scene.commands);
+        },
+
+        DOCUMENT_EVENTS: [ "keydown", "keyup", "visibilitychange" ],
+
+        CANVAS_EVENTS: [ "mousemove", "mousedown", "mouseup" ],
+
+        WINDOW_EVENTS: [ "popstate", "resize", "pageshow",
+                         "yuugamepadbuttondown", "yuugamepadbuttonup" ],
+
+        GESTURES: [
+            "touch", "release", "hold", "tap", "doubletap",
+            "dragstart", "drag", "dragend", "dragleft", "dragright",
+            "dragup", "dragdown", "swipe", "swipeleft", "swiperight",
+            "swipeup", "swipedown", "pinch", "pinchin", "pinchout"
+        ],
+
+        _dispatchSceneInput: function (name, args) {
+            var scenes = this._scenes;
+            for (var i = scenes.length - 1; i >= 0; --i) {
+                var scene = scenes[i];
+                var handler = scene.inputs[name];
+                if (handler && handler.apply(scene, args))
+                    return true;
+
+                // FIXME: This may be a heavy ad hoc solution for the
+                // multiple input layer problems in pwl6. Something
+                // like this isn't required or allowed for e.g.
+                // individual keys, why not?
+                //
+                // REALLY FIXME: This doesn't even work correctly for
+                // joystick events, a) because they're prefixed and b)
+                // because they are in WINDOW_EVENTS so you have to
+                // manually enumerate them or you also ignore e.g.
+                // resize.
+                else if (scene.inputs.consume
+                         && yf.contains(scene.inputs.consume, name))
+                    return false;
+            }
+            return false;
+        },
+
+        // Aside from the performance considerations, deferring
+        // resizing by multiple frames fixes mis-sizing during startup
+        // and fullscreen transition in node-webkit on Windows. (And
+        // probably similar bugs in other configurations.)
+        _doresize: yf.debounce(function () {
+            this._resized = true;
+        }, 500),
+
+        _dovisibilitychange: function (event) {
+            if (event.target.hidden)
+                this._stopRender();
+            else
+                this._startRender();
+        },
+
+        _dopageshow: function (event) {
+            if (!history.state)
+                history.pushState("yuu director", "");
+            this._stopRender();
+            if (!document.hidden)
+                this._startRender();
+        },
+
+        _dopopstate: function (event) {
+            var cmds = [];
+            if (this._dispatchSceneInput("back", [])
+                || (cmds = this.input.change("back"))) {
+                history.pushState("yuu director", "");
+                yf.each.call(this, this.execute, cmds);
+                yuu.stopPropagation(event, true);
+            } else {
+                history.back();
+            }
+        },
+
+        __dogesture: function (event) {
+            this._updateCaps(event.gesture.srcEvent.type.toLowerCase(), true);
+            var type = event.type.toLowerCase();
+            var p0 = yuu.deviceFromCanvas(event.gesture.startEvent.center);
+            var p1 = yuu.deviceFromCanvas(event.gesture.center);
+            if (this._dispatchSceneInput(type, [p0, p1]))
+                yuu.stopPropagation(event, true);
+        },
+
+        // TODO: This munges events, but also, InputState's mousemove
+        // etc. munge events, in a slightly different but still
+        // related and fragile way.
+        //
+        // Additionally, things run in a Scene handler won't
+        // affect the InputState's internal state - good for
+        // avoiding bind execution, bad for consistency. Even if
+        // a scene handles e.g. "keydown a", input.pressed.a
+        // should be true.
+        //
+        // This is compounded by the lack of actual use cases for any
+        // of the non-gesture events other than "back" and
+        // "mousemove".
+
+        _ARGS_FOR: {
+            keydown: function (event) {
+                return [yuu.keyEventName(event), {}];
+            },
+            keyup: function (event) {
+                return [yuu.keyEventName(event), {}];
+            },
+            mousemove: function (event) {
+                return [yuu.deviceFromCanvas(event)];
+            },
+            mouseup: function (event) {
+                return [event.button, yuu.deviceFromCanvas(event)];
+            },
+            mousedown: function (event) {
+                return [event.button, yuu.deviceFromCanvas(event)];
+            },
+            gamepadbuttondown: function (event) {
+                return [event.detail.gamepad,
+                        event.detail.button];
+            },
+            gamepadbuttonup: function (event) {
+                return [event.detail.gamepad, event.detail.button];
+            },
+        },
+
+        _updateCaps: function (type, definite) {
+            if (type.startsWith("mouse")) {
+                if (this._devices.mouse === undefined || definite)
+                    this._devices.mouse = Date.now();
+                this._devices.touch = this._devices.touch || false;
+            } else if (type.startsWith("touch")) {
+                this._devices.mouse = this._devices.mouse || false;
+                this._devices.touch = Date.now();
+                this._devices.keyboard = this._devices.keyboard || false;
+            } else if (type.startsWith("key")) {
+                this._devices.keyboard = Date.now();
+            } else if (type.startsWith("gamepad")) {
+                this._devices.gamepad = Date.now();
+                this._devices.touch = this._devices.touch || false;
+            }
+        },
+
+        preferredDevice: function (options) {
+            options = options || ["keyboard", "touch", "mouse", "gamepad"];
+            var devices = this._devices;
+            var best = yf.foldl(function (best, option) {
+                var dbest = devices[best];
+                var doption = devices[option];
+                return dbest === undefined && doption ? option
+                    : doption > dbest ? option : best;
+            }, options);
+            for (var i = 0; devices[best] === false && i < options.length; ++i)
+                if (devices[options[i]] !== false)
+                    best = options[i];
+            return best;
+        },
+
+        _doevent: function (event) {
+            var type = event.type.toLowerCase();
+            if (type.startsWith("yuu"))
+                type = type.slice(3);
+            var args = this._ARGS_FOR[type](event);
+            var cmds;
+            this._updateCaps(type, false);
+            if (this._dispatchSceneInput(type, args))
+                yuu.stopPropagation(event, true);
+            else if ((cmds = this.input[type].apply(this.input, args))) {
+                var ctx = yf.last(args);
+                yf.each.call(this, this.execute, cmds, yf.repeat(ctx, cmds.length));
+                yuu.stopPropagation(event, true);
+            }
+        },
+
+        _addListener: function (target, name, handler) {
+            handler = (handler || this["_do" + name] || this._doevent).bind(this);
+            this._events[name] = { target: target, handler: handler };
+            target.addEventListener(name, handler);
+        },
+
+        _removeListener: function (name) {
+            this._events[name].target.removeEventListener(
+                name, this._events[name].handler);
+            delete this._events[name];
+        },
+
+        tickHz: {
+            get: function () { return this._tickHz; },
+            set: function (hz) {
+                this._tickHz = hz;
+                this._tickCount = 0;
+                this._timerStart = 0;
+            }
+        },
+
+        currentTime: { get: function () {
+            return this._timerStart + 1000 * this._tickCount / this._tickHz;
+        } },
+
+        currentAudioTime: { get: function () {
+            /** Audio time of the current tick.
+             */
+            return (this.currentTime + this._audioOffset) / 1000;
+        } },
+
+        _startRender: function () {
+            if (this._rafId !== null)
+                return;
+            this._tickCount = 0;
+            this._timerStart = 0;
+            // GNU/Linux with node-webkit sizes things incorrectly on
+            // startup, so force a recalculating as soon as the render
+            // loop runs.
+            this._resized = true;
+            var director = this;
+            this._rafId = window.requestAnimationFrame(function _ (t) {
+                if (!director._timerStart) {
+                    director._timerStart = t;
+                    director._audioOffset = yuu.audio
+                        ? yuu.audio.currentTime * 1000 - t
+                        : 0;
+                }
+                director._rafId = window.requestAnimationFrame(_);
+                director.render(t);
+            });
+        },
+
+        _stopRender: function () {
+            if (this._rafId !== null)
+                window.cancelAnimationFrame(this._rafId);
+            this._rafId = null;
+        },
+
+        start: function () {
+            /** Begin ticking and rendering scenes */
+            yf.each(this._addListener.bind(this, window),
+                    this.WINDOW_EVENTS);
+            yf.each(this._addListener.bind(this, document),
+                    this.DOCUMENT_EVENTS);
+            yf.each(this._addListener.bind(this, yuu.canvas),
+                    this.CANVAS_EVENTS);
+
+            this._gesture = typeof Hammer !== "undefined"
+                ? new Hammer(yuu.canvas, { "tap_always": false,
+                                           "hold_timeout": 300 })
+            : { on: function () {}, off: function () {} };
+            this._gesture.on(this.GESTURES.join(" "), this._dogesture);
+
+            // Treat the back button as another kind of input event. Keep
+            // a token state on the stack to catch the event, and if no
+            // scene handles it, just go back one more.
+            //
+            // Because of browser session restore, state might already be
+            // on the stack. Throw it out if so.
+            if (!history.state)
+                history.pushState("yuu director", "");
+            else
+                history.replaceState("yuu director", "");
+            this._startRender();
+        },
+
+        stop: function () {
+            /** Stop ticking and rendering, clear all scenes */
+            this._stopRender();
+            yf.eachr(function (scene) { scene.done(); }, this._scenes);
+            this._scenes = [];
+            yf.each.call(this, this._removeListener, Object.keys(this._events));
+            this._gesture.off(this.GESTURES.join(" "), this._dogesture);
+            this._gesture = null;
+        },
+
+        message: function () {
+            /** Send a message to all entities/scenes, bottom to top */
+            this.entity0.message.apply(this.entity0, arguments);
+            var scenes = this._scenes;
+            for (var i = 0; i < scenes.length; ++i)
+                scenes[i].message.apply(scenes[i], arguments);
+        },
+
+        _takeScreenshot: function () {
+            var date = (new Date()).toLocaleString();
+            try {
+                yuu.downloadURL(
+                    yuu.canvas.toDataURL("image/png"),
+                    document.title + " (" + date + ").png");
+                this.toast("\uf030", 0.5, "screenshot");
+            } catch (exc) {
+                var dialog = yuu.showError(exc);
+                if (dialog)
+                    this.showOverlay(dialog.id);
+            }
+        },
+
+        render: function (t) {
+            /** Tick and render all scenes, bottom to top */
+            var i;
+
+            if (this._resized) {
+                this._dispatchSceneInput("resize", [yuu.canvas]);
+                this._resized = false;
+            }
+            
+            t = t - this._timerStart;
+            var oneTick = 1000.0 / this._tickHz;
+            while (oneTick * this._tickCount < t)
+                this.message("tick", oneTick * this._tickCount++, oneTick);
+            this.message("tock", (t % oneTick) / oneTick);
+            
+            yuu.gl.clear(yuu.gl.COLOR_BUFFER_BIT);
+            var scenes = this._scenes;
+            var cursor = "default";
+            for (i = 0; i < scenes.length; ++i) {
+                scenes[i].render();
+                cursor = scenes[i].cursor || cursor;
+            }
+
+            if (cursor !== yuu.canvas.style.cursor)
+                yuu.canvas.style.cursor = cursor;
+
+            for (i = 0; i < this._afterRender.length; ++i)
+                this._afterRender[i]();
+            this._afterRender.length = 0;
+        },
+
+        toast: yuu.cmd(function (markup, duration, id) {
+            var toasts = this._toasts;
+            id = "yuu-toast-" + id;
+            var toast = id ? document.querySelector("#" + id) : null;
+            duration = duration || 4;
+
+            if (!toast) {
+                toast = document.createElement("div");
+                toast.id = id;
+                toast.className = "yuu-toast yuu-fade";
+                document.body.appendChild(toast);
+            }
+            if (toasts[id]) {
+                clearTimeout(toasts[id]);
+                delete toasts[id];
+            }
+            toast.innerHTML = markup;
+            yuu.afterAnimationFrame(function () {
+                toast.className = "yuu-toast";
+            });
+
+            var to = setTimeout(function () {
+                toast.className = "yuu-toast yuu-fade";
+                toast.addEventListener("transitionend", function fade () {
+                    toast.removeEventListener("transitionend", fade);
+                    // Stop if the toast was revived between the
+                    // timeout event and transition end, i.e. while it
+                    // was fading out.
+                    if (id && toasts[id] !== to)
+                        return;
+                    toast.className += " yuu-squish";
+                    toast.addEventListener("transitionend", function squish () {
+                        toast.removeEventListener("transitionend", squish);
+                        if (id && toasts[id] === to) {
+                            delete toasts[id];
+                            toast.parentNode.removeChild(toast);
+                        }
+                    });
+                });
+            }, duration * 1000);
+            if (id)
+                toasts[id] = to;
+        }, "<markup> <duration?>", "show a toast message"),
+
+        showOverlay: yuu.cmd(function (id, animation, dismissKeys) {
+            var overlay = new yuu.Overlay(
+                document.getElementById(id), animation, dismissKeys);
+            this.pushScene(overlay);
+        }, "<overlay ID> <animation?> <dismissKeys?>", "show an HTML overlay"),
+
+        screenshot: yuu.cmd(function () {
+            this._afterRender.push(this._takeScreenshot.bind(this));
+        }, "take a screenshot"),
+
+        fullscreen: yuu.cmd(function (v) {
+            if (arguments.length > 0) {
+                yuu.fullscreen = !!v;
+                // Most browser/OS combinations will drop key events
+                // during the "transition to fullscreen" animation.
+                // This means the key to enter fullscreen is recorded
+                // as "stuck down" inside the input code, and pressing
+                // it again won't trigger exiting fullscreen, just
+                // clear the stuck bit - you would have to press it
+                // *again* to actually transition out of fullscreen.
+                //
+                // Obviously this is not good, and the chance of the
+                // player actually trying to do something meaningful
+                // during fullscreen transition is unlikely, so just
+                // blow away the internal state and act like
+                // everything the player does is new.
+                this.input.reset();
+            }
+            return yuu.fullscreen;
+        }, "<enabled?>", "enable/disable fullscreen"),
+
+        execute: { proxy: "_commandStack.execute" },
+    });
+
+    yuu.Scene = yT({
+        constructor: function () {
+            /** A collection of entities, a layer, keybinds, and commands
+
+                The single argument is as function that will be scalled
+                during construction with `this` as the newly-created
+                scene.
+
+            */
+            this.entity0 = new yuu.E();
+            this.layer0 = new yuu.Layer();
+            this.keybinds = new yuu.KeyBindSet(this.KEYBINDS);
+            this.commands = yuu.extractCommands(this);
+        },
+
+        addEntity: { proxy: "entity0.addChild" },
+        removeEntity: { proxy: "entity0.removeChild" },
+        addEntities: { proxy: "entity0.addChildren" },
+        removeEntities: { proxy: "entity0.removeChildren" },
+        message: { proxy: "entity0.message" },
+
+        init: function (director) {
+            /** Called when the director starts this scene */
+        },
+
+        done: function () {
+            /** Called when the director stops this scene */
+        },
+
+        render: function () {
+            /** Queue renderables from the entities and render each layer */
+            this.message("queueRenderables", this.layer0.rdros);
+            this.layer0.render();
+            this.layer0.clear();
+        },
+
+        inputs: {},
+        KEYBINDS: {}
+    });
+
+    yuu.Overlay = yT(yuu.Scene, {
+        constructor: function (element, animation, dismissKeys) {
+            yuu.Scene.call(this);
+            this.dismissKeys = dismissKeys
+                || (element.getAttribute("data-yuu-dismiss-key") || "").split(" ");
+            this.animation = animation
+                || element.getAttribute("data-yuu-animation")
+                || "yuu-from-top";
+            this.element = element;
+            this.className = element.className;
+            this._keydown = function (event) {
+                var name = yuu.keyEventName(event);
+                if (this.inputs.keydown.call(this, name))
+                    yuu.stopPropagation(event);
+            }.bind(this);
+        },
+
+        inputs: {
+            back: function () { this.dismiss(); return true; },
+            keydown: function (key) {
+                if (yf.contains(this.dismissKeys, key))
+                    this.dismiss();
+                return true;
+            },
+            touch: function () { this.dismiss(); return true; },
+            mousedown: function () { this.dismiss(); return true; },
+        },
+
+        init: function (director) {
+            var element = this.element;
+            var className = this.className;
+            var elements = element.querySelectorAll("[data-yuu-command]");
+
+            yf.each(function (element) {
+                var command = element.getAttribute("data-yuu-command");
+                switch (element.tagName.toLowerCase()) {
+                case "input":
+                    switch (element.type.toLowerCase()) {
+                    case "range":
+                        element.value = director.execute(command);
+                        break;
+                    case "checkbox":
+                        var res = !!director.execute(command);
+                        element.checked = res;
+                        break;
+                    }
+                    break;
+                }
+            }, elements);
+
+            yf.each(function (a) {
+                a.onclick = function (event) {
+                    yuu.openURL(this.href);
+                    yuu.stopPropagation(event, true);
+                };
+            }, element.querySelectorAll("a[href]:not([yuu-href-internal])"));
+
+            this._director = director;
+
+            element.className = className + " " + this.animation;
+            element.style.display = "block";
+            element.tabIndex = 0;
+            element.focus();
+            element.addEventListener("keydown", this._keydown);
+
+            yuu.afterAnimationFrame(function () {
+                element.className = className;
+            });
+        },
+
+        dismiss: yuu.cmd(function () {
+            var element = this.element;
+            var className = this.className;
+            var director = this._director;
+            var scene = this;
+            element.className = className + " " + this.animation;
+            element.addEventListener("transitionend", function _ () {
+                element.removeEventListener("transitionend", _);
+                director.removeScene(scene);
+            });
+        }, "", "dismiss this overlay"),
+
+        done: function () {
+            this.element.style.display = "none";
+            this.element.tabIndex = -1;
+            this.element.className = this.className;
+            this.element.removeEventListener("keydown", this._keydown);
+            this._director = null;
+            yuu.canvas.focus();
+        },
+
+        KEYBINDS: {
+            "escape": "dismiss"
+        }
+    });
+
+    yuu.registerInitHook(function () {
+        var elements = document.querySelectorAll("[data-yuu-command]");
+
+        function handleElement (event) {
+            /*jshint validthis:true */
+            /* `this` comes from being a DOM element event handler. */
+            var command = this.getAttribute("data-yuu-command");
+            switch (this.tagName.toLowerCase()) {
+            case "input":
+                switch (this.type.toLowerCase()) {
+                case "range":
+                    command += " " + this.value;
+                    break;
+                case "checkbox":
+                    command += " " + (this.checked ? "1" : "0");
+                    break;
+                }
+                break;
+            }
+            yuu.director.execute(command);
+            yuu.stopPropagation(event);
+        }
+
+        yf.each(function (element) {
+            switch (element.tagName.toLowerCase()) {
+            case "input":
+                switch (element.type.toLowerCase()) {
+                case "range":
+                    element.oninput = handleElement;
+                    element.onchange = handleElement;
+                    break;
+                case "checkbox":
+                    element.onchange = handleElement;
+                    break;
+                }
+                break;
+            case "div":
+            case "span":
+                element.onclick = handleElement;
+                element.onkeydown = function (event) {
+                    var name = yuu.keyEventName(event);
+                    if (name === "space" || name === "return")
+                        handleElement.call(this, event);
+                };
+                break;
+            default:
+                element.onclick = handleElement;
+                break;
+            }
+        }, elements);
+
+        yuu.defaultKeybinds.bind("control+`", "showDevTools");
+        yuu.defaultKeybinds.bind("f11", "++fullscreen");
+        yuu.defaultKeybinds.bind("f12", "screenshot");
+        yuu.defaultKeybinds.bind(
+            "control+s", "++mute && toast \uf026 1 mute || toast \uf028 1 mute");
+
+        var director = yuu.director = new yuu.Director();
+            /** The standard director */
+
+        yuu.registerInitHook(function () {
+            return yuu.ready(director.scenes);
+        });
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/gfx.js b/src/yuu/gfx.js
new file mode 100644 (file)
index 0000000..2c3088e
--- /dev/null
@@ -0,0 +1,665 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+    var gl;
+    var canvas;
+
+    var dpr = yuu.DPR = this.devicePixelRatio || 1;
+
+    yT.defineProperty(Int8Array.prototype, "GL_TYPE", 0x1400);
+    yT.defineProperty(Uint8Array.prototype, "GL_TYPE", 0x1401);
+    yT.defineProperty(Int16Array.prototype, "GL_TYPE", 0x1402);
+    yT.defineProperty(Uint16Array.prototype, "GL_TYPE", 0x1403);
+    yT.defineProperty(Int32Array.prototype, "GL_TYPE", 0x1404);
+    yT.defineProperty(Uint32Array.prototype, "GL_TYPE", 0x1405);
+    yT.defineProperty(Float32Array.prototype, "GL_TYPE", 0x1406);
+        /** Patch the WebGL type onto arrays for data-driven access later
+
+            Values from https://www.khronos.org/registry/webgl/specs/1.0/.
+
+            See also notes in pre on Safari's typed array problems.
+        */
+
+    yuu.uniform = function (location, value) {
+        /** Set a uniform in the active program
+
+            The type of the uniform is automatically determined from
+            the value:
+
+            * Typed integer arrays of length 1-4 call uniform[1-4]iv
+            * Other sequences of length 1-4 call uniform[1-4]fv
+            * Sequences of length 9 or 16 call uniformMatrix[3-4]fv
+            * Non-sequences call uniform1fv (even if the parameter
+              is a valid integer)
+            * Sequences of other lengths throw a TypeError
+
+            It is not possible to call uniformMatrix2fv via this
+            function.
+        */
+        switch (value.constructor) {
+        case Int8Array:
+        case Uint8Array:
+        case Int16Array:
+        case Uint16Array:
+        case Int32Array:
+        case Uint32Array:
+            switch (value.length) {
+            case 1: return gl.uniform1iv(location, value);
+            case 2: return gl.uniform2iv(location, value);
+            case 3: return gl.uniform3iv(location, value);
+            case 4: return gl.uniform4iv(location, value);
+            default: throw new TypeError("unexpected array length");
+            }
+            break;
+        default:
+            switch (value.length) {
+            case 1: return gl.uniform1fv(location, value);
+            case 2: return gl.uniform2fv(location, value);
+            case 3: return gl.uniform3fv(location, value);
+            case 4: return gl.uniform4fv(location, value);
+            case 9: return gl.uniformMatrix3fv(location, false, value);
+            case 16: return gl.uniformMatrix4fv(location, false, value);
+            case undefined: return gl.uniform1f(location, value);
+            default: throw new TypeError("unexpected array length");
+            }
+        }
+    };
+
+    function isShaderSource (src) {
+        return src.indexOf("\n") >= 0 || yf.last(src.trim()) === ";";
+    }
+
+    var FRAGMENT_SHADER = 0x8B30;
+    var VERTEX_SHADER = 0x8B31;
+    var EXTS = {};
+    EXTS[FRAGMENT_SHADER] = "frag";
+    EXTS[VERTEX_SHADER] = "vert";
+
+    function compile (type, srcs) {
+        function getSource (src) {
+            return isShaderSource(src)
+                ? Promise.resolve(src)
+                : yuu.GET(yuu.resourcePath(src, "shaders", EXTS[type]));
+        }
+        return Promise.all(yf.map(getSource, srcs))
+            .then(function (srcs) {
+                var src = srcs.join("\n");
+                var shader = gl.createShader(type);
+                gl.shaderSource(shader, src);
+                gl.compileShader(shader);
+                if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+                    var log = gl.getShaderInfoLog(shader);
+                    throw new Error(
+                        "Shader compile error:\n\n"    + src + "\n\n" + log);
+                }
+                return shader;
+            });
+    }
+
+    yuu.ShaderProgram = yT({
+        constructor: function (vs, fs) {
+            /** A linked program of vertex and fragment shaders
+                
+                vs and fs are arrays of vertex and fragment shader source
+                code or URLs.
+            */
+            fs = fs || ["yuu/@default"];
+            vs = vs || ["yuu/@default"];
+            var id = this.id = gl.createProgram();
+            var attribs = this.attribs = {};
+            var uniforms = this.uniforms = {};
+            this.ready = Promise.all([compile(VERTEX_SHADER, vs),
+                                      compile(FRAGMENT_SHADER, fs)])
+                .then(function (shaders) {
+                    yf.each(gl.attachShader.bind(gl, id), shaders);
+                    gl.linkProgram(id);
+                    if (!gl.getProgramParameter(id, gl.LINK_STATUS))
+                        throw new Error("Shader link error: "
+                                        + gl.getProgramInfoLog(id));
+                    return id;
+                }).catch(function (exc) {
+                    yuu.showError(exc);
+                    this.id = yuu.ShaderProgram.DEFAULT.id;
+                    this.attribs = yuu.ShaderProgram.DEFAULT.attribs;
+                    this.uniforms = yuu.ShaderProgram.DEFAULT.uniforms;
+                    throw exc;
+                }.bind(this)).then(function (id) {
+                    this.id = id;
+                    yf.irange(function (i) {
+                        var name = gl.getActiveAttrib(id, i).name;
+                        attribs[name] = gl.getAttribLocation(id, name);
+                    }, gl.getProgramParameter(id, gl.ACTIVE_ATTRIBUTES));
+                    yf.irange(function (i) {
+                        var name = gl.getActiveUniform(id, i).name;
+                        uniforms[name] = gl.getUniformLocation(id, name);
+                    }, gl.getProgramParameter(id, gl.ACTIVE_UNIFORMS));
+                    return this;
+                }.bind(this));
+        },
+
+        setUniforms: function () {
+            /** Set the values of program uniforms
+
+                The arguments are any number of objects mapping
+                uniform names to values (floats, vec3s, etc.).
+            */
+            for (var i = 0; i < arguments.length; ++i)
+                for (var name in arguments[i])
+                    yuu.uniform(this.uniforms[name], arguments[i][name]);
+        },
+
+        setAttribPointers: function (buffer) {
+            /** Bind the contents of a vertex buffer to attributes
+
+                `buffer` is (or is like) a yuu.VertexBuffer instance.
+            */
+            for (var name in this.attribs)
+                gl.vertexAttribPointer(
+                    this.attribs[name],
+                    buffer.spec.attribs[name].elements,
+                    buffer.spec.attribs[name].View.prototype.GL_TYPE,
+                    false, 0, buffer.arrays[name].byteOffset);
+        }
+    });
+
+    // This function is easier to read than a giant lookup table
+    // ({ textureWrapS: "TEXTURE_WRAP_S", ... x100 }) but slower.
+    function glEnum (gl, name) {
+        return gl[name.replace(/([A-Z]+)/g, "_$1").toUpperCase()];
+    }
+
+    function glScopedEnum (scope, gl, name) {
+        var value = glEnum(gl, scope + "_" + name);
+        if (value === undefined)
+            value = glEnum(gl, name);
+        return value;
+    }
+
+    var glTextureEnum = glScopedEnum.bind(null, "texture");
+
+    yuu.Texture = yuu.Caching(yT({
+        constructor: function (path, overrideOptions) {
+            /** A 2D texture
+
+                The texture is set to a 1x1 white texture until it is
+                loaded (or if loading fails).
+            */
+            var options = {};
+            yf.ipairs(function (k, v) {
+                options[glTextureEnum(gl, k)] = glEnum(gl, v);
+            }, TEXTURE_DEFAULTS);
+            yf.ipairs(function (k, v) {
+                options[glTextureEnum(gl, k)] = glEnum(gl, v);
+            }, overrideOptions || {});
+            
+            if (!path) {
+                var data = new Uint8Array([255, 255, 255, 255]);
+                this.id = gl.createTexture();
+                this.width = this.height = 1;
+                this.src = "default / fallback 1x1 white texture";
+                gl.bindTexture(gl.TEXTURE_2D, this.id);
+                gl.texImage2D(
+                    gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
+                    gl.RGBA, gl.UNSIGNED_BYTE, data);
+                gl.texParameteri(
+                    gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+                gl.texParameteri(
+                    gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+                gl.bindTexture(gl.TEXTURE_2D, null);
+                this.ready = Promise.resolve(this);
+                return;
+            }
+
+            path = yuu.resourcePath(path, "images", "png");
+            this.id = yuu.Texture.DEFAULT.id;
+            this.src = path;
+            this.width = yuu.Texture.DEFAULT.width;
+            this.height = yuu.Texture.DEFAULT.height;
+
+            this.ready = yuu.Image(path).then(function (img) {
+                var id = gl.createTexture();
+                gl.bindTexture(gl.TEXTURE_2D, id);
+                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
+                gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
+                for (var opt in options)
+                    gl.texParameteri(gl.TEXTURE_2D, opt, options[opt]);
+                gl.texImage2D(
+                    gl.TEXTURE_2D, 0, gl.RGBA,
+                    gl.RGBA, gl.UNSIGNED_BYTE, img);
+                gl.bindTexture(gl.TEXTURE_2D, null);
+                this.id = id;
+                this.width = img.width;
+                this.height = img.height;
+                this.src = img.src;
+                return this;
+            }.bind(this)).catch(function (e) {
+                this.src = "Error loading " + path + ": " + e;
+                yuu.log("errors", this.src);
+                gl.bindTexture(gl.TEXTURE_2D, null);
+                throw e;
+            }.bind(this));
+        }
+    }));
+
+    var TEXTURE_DEFAULTS = yuu.Texture.DEFAULTS = {
+        magFilter: "linear",
+        minFilter: "linear",
+        wrapS: "clampToEdge",
+        wrapT: "clampToEdge"
+    };
+
+    yuu.Material = yuu.Caching(yT({
+        constructor: function (texture, program, uniforms) {
+            /** A material is a combination of a texture and shader program */
+            if (yf.isString(texture))
+                texture = new yuu.Texture(texture);
+            this.texture = texture || yuu.Texture.DEFAULT;
+            this.program = program || yuu.ShaderProgram.DEFAULT;
+            this.ready = yuu.ready([this.texture, this.program], this);
+            this.uniforms = uniforms || {};
+        },
+
+        enable: function (uniforms) {
+            /** Enable this material and its default parameters */
+            gl.bindTexture(gl.TEXTURE_2D, this.texture.id);
+            gl.useProgram(this.program.id);
+            for (var attrib in this.program.attribs)
+                gl.enableVertexAttribArray(this.program.attribs[attrib]);
+            this.program.setUniforms(this.uniforms, uniforms);
+        },
+
+        disable: function () {
+            /** Disable this material */
+            gl.bindTexture(gl.TEXTURE_2D, null);
+            gl.useProgram(null);
+            for (var attrib in this.program.attribs)
+                gl.disableVertexAttribArray(this.program.attribs[attrib]);
+        }
+    }));
+
+    yuu.VertexAttribSpec = function (spec) {
+        /** Ordering and types for vertex buffer layout
+
+            Interleaved vertices (e.g. VTCVTCVTC) are not currently
+            supported, as ArrayBufferViews are not able to manage a buffer
+            with this kind of layout.
+        */
+        var byteOffset = 0;
+        this.attribs = {};
+        spec.forEach(function (a) {
+            var name = a.name;
+            var elements = a.elements;
+            var View = a.View || Float32Array;
+            this.attribs[name] = { elements: elements,
+                                   byteOffset: byteOffset,
+                                   View: View };
+            byteOffset += elements * View.BYTES_PER_ELEMENT;
+        }, this);
+        this.bytesPerVertex = byteOffset;
+    };
+
+    yuu.V3T2C4_F = new yuu.VertexAttribSpec([
+        /** vec3 position; vec2 texCoord; vec4 color; */
+        { name: "position", elements: 3 },
+        { name: "texCoord", elements: 2 },
+        { name: "color", elements: 4 }
+    ]);
+    
+    yuu.IndexBuffer = yT({
+        constructor: function (maxIndex, length) {
+            this._capacity = -1;
+            this._maxIndex = maxIndex;
+            this.buffer = null;
+            this.type = null;
+            this.length = length;
+            this._glBuffer = gl.createBuffer();
+            this.dirty = true;
+        },
+
+        bindBuffer: function () {
+            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._glBuffer);
+            if (this.dirty) {
+                this.dirty = false;
+                gl.bufferData(
+                    gl.ELEMENT_ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW);
+            }
+        },
+
+        GL_TYPE: { alias: "buffer.GL_TYPE" },
+
+        maxIndex: {
+            get: function () { return this._maxIndex; },
+            set: function (maxIndex) {
+                var Array = yuu.IndexBuffer.Array(maxIndex);
+                if (maxIndex > this._maxIndex
+                    && Array !== this.buffer.constructor) {
+                    var buffer = new Array(this._capacity);
+                    if (this.buffer)
+                        buffer.set(this.buffer);
+                    this.buffer = buffer;
+                    this.dirty = true;
+                }
+                this._maxIndex = maxIndex;
+            }
+        },
+
+        length: {
+            get: function () { return this._length; },
+            set: function (count) {
+                if (count > this._capacity) {
+                    var Array = yuu.IndexBuffer.Array(this._maxIndex);
+                    var buffer = new Array(count);
+                    if (this.buffer)
+                        buffer.set(this.buffer);
+                    this.buffer = buffer;
+                    this._capacity = count;
+                    this.dirty = true;
+                }
+                this._length = count;
+            }
+        }
+    });
+
+    yuu.IndexBuffer.Array = function (maxIndex) {
+        if (maxIndex < 0 || maxIndex >= (256 * 256 * 256 * 256))
+            throw new Error("invalid maxIndex index: " + maxIndex);
+        else if (maxIndex < (1 << 8))
+            return Uint8Array;
+        else if (maxIndex < (1 << 16))
+            return Uint16Array;
+        else
+            return Uint32Array;
+    };
+
+    yuu.VertexBuffer = yT({
+        constructor: function (spec, vertexCount) {
+            /** A buffer with a specified vertex format and vertex count
+
+                The individual vertex attribute array views from the
+                attribute specification are available via the .arrays
+                property, e.g. v.arrays.position. The underlying
+                buffer is available as v.buffer.
+
+                The vertex count may be changed after creation and the
+                buffer size and views will be adjusted. If you've
+                grown the buffer, you will need to refill all its
+                data. Shrinking it will truncate it.
+            */
+            this.spec = spec;
+            this._vertexCapacity = -1;
+            this.buffer = null;
+            this.arrays = {};
+            this.vertexCount = vertexCount;
+            this._glBuffer = gl.createBuffer();
+            this.dirty = true;
+        },
+
+        bindBuffer: function () {
+            gl.bindBuffer(gl.ARRAY_BUFFER, this._glBuffer);
+            if (this.dirty) {
+                this.dirty = false;
+                gl.bufferData(gl.ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW);
+            }
+        },
+
+        subdata: function (begin, length) {
+            return new yuu.VertexBuffer.SubData(this, begin, begin + length);
+        },
+        
+        vertexCount: {
+            get: function () { return this._vertexCount; },
+            set: function (count) {
+                if (count > this._vertexCapacity) {
+                    var buffer = new ArrayBuffer(
+                        this.spec.bytesPerVertex * count);
+                    var arrays = {};
+                    yf.ipairs.call(this, function (name, attrib) {
+                        arrays[name] = new attrib.View(
+                            buffer, attrib.byteOffset * count,
+                            attrib.elements * count);
+                        if (this.arrays[name])
+                            arrays[name].set(this.arrays[name]);
+                    }, this.spec.attribs);
+                    this.buffer = buffer;
+                    this.arrays = arrays;
+                    this._vertexCapacity = count;
+                    this.dirty = true;
+                }
+                this._vertexCount = count;
+            }
+        }
+    });
+
+    yuu.VertexBuffer.SubData = yT({
+        constructor: function (parent, begin, end) {
+            var arrays = this.arrays = {};
+            this._parent = parent;
+            this.spec = parent.spec;
+            this.buffer = parent.buffer;
+            yT.defineProperty(this, "vertexCount", end - begin);
+            for (var attrib in parent.arrays) {
+                var s = parent.spec.attribs[attrib].elements;
+                arrays[attrib] = parent.arrays[attrib].subarray(
+                    begin * s, end * s);
+            }
+        },
+
+        dirty: { alias: "_parent.dirty" }
+    });
+
+    var rgbToHsl = yuu.rgbToHsl = yf.argcd(
+        /** Convert RBG [0, 1] to HSL [0, 1]. */
+        function (rgb) { return rgbToHsl.apply(null, rgb); },
+        function (r, g, b, a) {
+            var hsl = rgbToHsl(r, g, b);
+            hsl[3] = a;
+            return hsl;
+        },
+        function (r, g, b) {
+            var max = Math.max(r, g, b);
+            var min = Math.min(r, g, b);
+            var h, s, l = (max + min) / 2;
+
+            if (max === min) {
+                h = s = 0;
+            } else {
+                var d = max - min;
+                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+                switch (max) {
+                case r:
+                    h = (g - b) / d + (g < b ? 6 : 0);
+                    break;
+                case g:
+                    h = (b - r) / d + 2;
+                    break;
+                case b:
+                    h = (r - g) / d + 4;
+                    break;
+                }
+                h /= 6;
+            }
+            
+            return [h, s, l];
+        }
+    );
+
+    var hslToRgb = yuu.hslToRgb = yf.argcd(
+        /** Convert HSL [0, 1] to RGB [0, 1]. */
+        function (hsl) { return hslToRgb.apply(null, hsl); },
+        function (h, s, l, a) {
+            var rgb = hslToRgb(h, s, l);
+            rgb[3] = a;
+            return rgb;
+        },
+        function (h, s, l) {
+            var r, g, b;
+            
+            function hToC (p, q, t) {
+                if (t < 0)
+                    t += 1;
+                if (t > 1)
+                    t -= 1;
+                if (t < 1 / 6)
+                    return p + (q - p) * 6 * t;
+                else if (t < 1 / 2)
+                    return q;
+                else if (t < 2 / 3)
+                    return p + (q - p) * (2/3 - t) * 6;
+                else
+                    return p;
+            }
+            
+            if (s === 0) {
+                r = g = b = l;
+            } else {
+                var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+                var p = 2 * l - q;
+                r = hToC(p, q, h + 1 / 3);
+                g = hToC(p, q, h);
+                b = hToC(p, q, h - 1 / 3);
+            }
+            
+            return [r, g, b];
+        }
+    );
+
+    var deviceFromCanvas = yuu.deviceFromCanvas = yf.argcd(
+        /** Convert a point from client to normalized device space
+
+            Normalized device space ranges from [-1, -1] at the
+            bottom-left of the viewport to [1, 1] at the top-right.
+            (This is the definition of the space, _not_ bounds on the
+            return value, as events can happen outside the viewport or
+            even outside the canvas.)
+        */
+        function (p) {
+            return deviceFromCanvas(p.x || p.pageX || p[0] || 0,
+                                    p.y || p.pageY || p[1] || 0);
+        },
+        function (x, y) {
+            x -= canvas.offsetLeft;
+            y -= canvas.offsetTop;
+            x /= canvas.clientWidth;
+            y /= canvas.clientHeight;
+            // xy is now in [0, 1] page space.
+
+            x *= canvas.width;
+            y *= canvas.height;
+            // xy now in canvas buffer space.
+
+            var vp = gl.getParameter(gl.VIEWPORT);
+            var hvpw = vp[2] / 2;
+            var hvph = vp[3] / 2;
+            x = (x - vp[0] - hvpw) / hvpw;
+            y = (y - vp[1] - hvph) / -hvph;
+            // xy now in normalized device space.
+
+            return {
+                x: x, 0: x,
+                y: y, 1: y,
+                inside: Math.abs(x) <= 1 && Math.abs(y) <= 1
+            };
+        }
+    );
+
+    yuu.viewport = new yuu.AABB();
+
+    function onresize () {
+        var resize = canvas.getAttribute("data-yuu-resize") !== null;
+        var width = +canvas.getAttribute("data-yuu-width");
+        var height = +canvas.getAttribute("data-yuu-height");
+
+        if (resize) {
+            canvas.width = canvas.clientWidth * dpr;
+            canvas.height = canvas.clientHeight * dpr;
+        }
+
+        var vw = canvas.width;
+        var vh = canvas.height;
+        if (width && height) {
+            var aspectRatio = width / height;
+            if (vw / vh > aspectRatio)
+                vw = vh * aspectRatio;
+            else
+                vh = vw / aspectRatio;
+        }
+        var vx = (canvas.width - vw) / 2;
+        var vy = (canvas.height - vh) / 2;
+        gl.viewport(vx, vy, vw, vh);
+        yuu.viewport = new yuu.AABB(vx, vy, vx + vw / dpr, vy + vh / dpr);
+    }
+
+    yuu.afterAnimationFrame = function (f) {
+        /* DOM class modifications intended to trigger transitions
+           must be delayed for at least one frame after the element is
+           created, i.e. after it has gone through at least one full
+           repaint.
+        */
+        window.requestAnimationFrame(function () {
+            setTimeout(f, 0);
+        });
+    };
+
+    yuu.registerInitHook(function (options) {
+        var bgColor = options.backgroundColor || [0.0, 0.0, 0.0, 0.0];
+
+        canvas = this.canvas = document.getElementById("yuu-canvas");
+        var glOptions = {
+            alpha: options.hasOwnProperty("alpha")
+                ? options.alpha : bgColor[3] !== 1.0,
+            antialias: options.hasOwnProperty("antialias")
+                ? options.antialias : true
+        };
+        if (!window.HTMLCanvasElement)
+            throw new Error("<canvas> isn't supported.");
+        gl = this.gl = canvas.getContext("webgl", glOptions)
+            || canvas.getContext("experimental-webgl", glOptions);
+        if (!gl)
+            throw new Error("WebGL isn't supported.");
+
+        canvas.focus();
+
+        window.addEventListener('resize', onresize);
+        onresize();
+
+        this.ShaderProgram.DEFAULT = new this.ShaderProgram();
+        this.Texture.DEFAULT = new this.Texture();
+        this.Material.DEFAULT = new this.Material();
+
+        gl.clearColor.apply(gl, bgColor);
+        gl.disable(gl.DEPTH_TEST);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+        gl.enable(gl.BLEND);
+        gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
+        gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
+    });
+
+    var gui = yuu.require("nw.gui");
+    yT.defineProperty(yuu, "fullscreen", {
+        get: function () {
+            return gui
+                ? gui.Window.get().isFullscreen
+                : !!(document.fullscreenElement
+                     || document.mozFullScreenElement);
+        },
+        set: function (v) {
+            if (gui)
+                gui.Window.get().isFullscreen = !!v;
+            else if (v)
+                document.body.requestFullscreen();
+            else
+                document.exitFullscreen();
+        }
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/input.js b/src/yuu/input.js
new file mode 100644 (file)
index 0000000..ba58faf
--- /dev/null
@@ -0,0 +1,268 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+
+    yuu.KEY_NAMES = {
+        32: "space",
+        13: "return",
+        16: "shift",
+        18: "alt",
+        17: "control",
+
+        37: "left",
+        38: "up",
+        39: "right",
+        40: "down",
+        9: "tab",
+        27: "escape",
+        8: "backspace",
+        191: "slash",
+        192: "`",
+    };
+
+    // Fill in the key name tables.
+    for (var i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); ++i)
+        yuu.KEY_NAMES[i] = String.fromCharCode(i).toLowerCase();
+    for (i = "0".charCodeAt(0); i <= "9".charCodeAt(0); ++i)
+        yuu.KEY_NAMES[i] = String.fromCharCode(i);
+    for (i = 1; i <= 12; ++i)
+        yuu.KEY_NAMES[111 + i] = "f" + i;
+
+    function splitKeys (keystring) {
+        return keystring.toLowerCase().split("+").sort();
+    }
+
+    var KeyBind = yT({
+        constructor: function (keystring, command) {
+            /** An individual key to command binding.
+
+                The key string is e.g. "a", "control+f", "left+alt+z".
+                "f+control" is equivalent to "control+f", and binding one
+                in a set will override the other.  */
+            this.keystring = keystring;
+            this.command = command;
+            this.keys = splitKeys(keystring);
+        },
+
+        uses: function (name) {
+            /** True if the given key name is relevant to this binding. */
+            return yf.contains(this.keys, name.toLowerCase());
+        },
+
+        on: function (pressed) {
+            /** True if all keys in this binding are pressed. */
+            return yf.every.call(pressed, yf.getter, this.keys);
+        }
+    });
+
+    function longerBind (a, b) {
+        return a.keys.length > b.keys.length ? a : b;
+    }
+
+    function longestBind (binds) {
+        return yf.foldl(longerBind, binds);
+    }
+
+    function isActivate (bind) {
+        return bind.command[0] === '+' && bind.command[1] !== '+';
+    }
+
+    function anticommand (bind) {
+        return "-" + bind.command.substring(1);
+    }
+
+    yuu.KeyBindSet = yT({
+        constructor: function (binds) {
+            /** A group of key bindings.
+
+                A set may only have one bind per key combination
+                (regardless of order) at a time. Binding already-bound
+                keys to a different command will overwrite, not duplicate,
+                that binding.
+            */
+            this.binds = [];
+            yf.ipairs.call(this, this.bind, binds || {});
+        },
+
+        bind: function (keystring, command) {
+            /** Bind keys to a command in this set. */
+            var bind = new KeyBind(keystring, command);
+            this.binds = this.binds.filter(function (b) {
+                return !yf.seqEqual(b.keys, bind.keys);
+            }).concat(bind);
+        },
+
+        unbind: function (keystring) {
+            /** Unbind keys from this set. */
+            var keys = splitKeys(keystring);
+            this.binds = this.binds.filter(function (b) {
+                return !yf.seqEqual(b.keys, keys);
+            });
+        }
+    });
+
+    yuu.keyEventName = function (event) {
+        return yuu.keyCodeName(event.keyCode, event.key || event.keyIdentifier);
+    };
+
+    yuu.keyCodeName = function (code, defaultName) {
+        if (defaultName)
+            defaultName = defaultName.toLowerCase();
+        if (defaultName === "unidentified")
+            defaultName = null;
+        var name = yuu.KEY_NAMES[code] || defaultName;
+        if (!name)
+            name = "key:" + code;
+        return name;
+    };
+
+    yuu.InputState = yT({
+        constructor: function (bindsets) {
+            this._bindsets = yf.slice(bindsets || [yuu.defaultKeybinds]);
+            this.pressed = {};
+               /** The current state of each key; 0 if not pressed,
+                   the time it was pressed if it is pressed. */
+        },
+
+        push: function (bindset) {
+            /** Add a key bind set to the handler stack. */
+            this._bindsets = this._bindsets.concat(bindset);
+        },
+
+        remove: function (bindset) {
+            /** Remove a key bind set from the handler stack. */
+            this._bindsets = yf.without(this._bindsets, bindset);
+        },
+
+        insertBefore: function (bindset, before) {
+            this._bindsets = yf.insertBefore(
+                this._bindsets.slice(), bindset, before);
+        },
+
+        _triggeredBinds: function (name) {
+            var pressed = this.pressed;
+            function triggered (bind) {
+                return bind.uses(name) && bind.on(pressed);
+            }
+            var binds = [];
+            yf.each(function (bindset) {
+                binds.push.apply(binds, yf.filter(triggered, bindset.binds));
+            }, this._bindsets);
+            return binds;
+        },
+
+        _down: function (name) {
+            /** Mark the input as down, return an array of commands.
+
+                This array is always of length 1 for down/change events.
+
+                Returns null if no binds were triggered, which is slightly
+                different than binds being triggered but no commands are
+                to be executed.
+            */
+            var pressed = this.pressed[name];
+            this.pressed[name] = Date.now();
+            var bind = longestBind(this._triggeredBinds(name));
+            return bind ? pressed ? [] : [bind.command] : null;
+        },
+
+        _up: function (name) {
+            /** Mark the input as down, return an array of commands.
+
+                Only binds for commands with the special + form are
+                returned on release, and the + is converted to a -.
+
+                Returns null if no binds were triggered, which is slightly
+                different than binds being triggered but no commands are
+                to be executed.
+            */
+            if (!this.pressed[name])
+                return null;
+            var cmds = yf.map(anticommand, yf.filter(
+                isActivate, this._triggeredBinds(name)));
+            this.pressed[name] = 0;
+            return cmds.length ? cmds : null;
+        },
+
+        change: function (name) {
+            /** Mark the input as changed, return an array of commands.
+
+                `change` is for inputs that do not have meaningful
+                "down" or "up" states, like moving a mouse. Instead,
+                it fires when the input's state changes - e.g. when
+                the x and y position change.
+
+                The `pressed` table remains unmodified as a result of
+                `change` inputs. Like `down`, `change` returns only the
+                first match it finds.
+            */
+            this.pressed[name] = Date.now();
+            var bind = longestBind(this._triggeredBinds(name));
+            this.pressed[name] = 0;
+            return bind ? [bind.command] : null;
+        },
+
+        keydown: { proxy: "_down" },
+        keyup: { proxy: "_up" },
+
+        gamepadbuttondown: function (gamepad, button) {
+            return this._down("gamepad" + gamepad.index + "button" + button)
+                || this._down("gamepadbutton" + button);
+        },
+
+        gamepadbuttonup: function (gamepad, button) {
+            return this._up("gamepad" + gamepad.index + "button" + button)
+                || this._up("gamepadbutton" + button);
+        },
+
+        mousemove: function () {
+            return this.change("mousemove");
+        },
+
+        mousedown: function (button) {
+            return this._down("mouse" + button);
+        },
+
+        mouseup: function (button) {
+            return this._up("mouse" + button);
+        },
+
+        reset: function () {
+            this.pressed = {};
+        }
+    });
+
+    yuu.stopPropagation = function stopPropagation (event, preventDefault) {
+        event.stopPropagation();
+        if (preventDefault)
+            event.preventDefault();
+        if (event.stopImmediatePropagation)
+            event.stopImmediatePropagation();
+        if (event.gesture && event.gesture !== event)
+            stopPropagation(event.gesture, preventDefault);
+    };
+
+    yuu.registerInitHook(function () {
+        yuu.defaultCommands.bind = yuu.cmd(function (key, command) {
+            yuu.defaultKeybinds.bind(key, command);
+        }, "<key> <command>", "bind a key to a command");
+
+        yuu.defaultCommands.unbind = yuu.cmd(function (key) {
+            yuu.defaultKeybinds.unbind(key);
+        }, "<key>", "unbind a key");
+
+        yuu.defaultKeybinds = new yuu.KeyBindSet();
+            /** The default / debugging bind set */
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/pre.js b/src/yuu/pre.js
new file mode 100644 (file)
index 0000000..9fd7f5c
--- /dev/null
@@ -0,0 +1,338 @@
+/* This is free and unencumbered software released into the public
+   domain. To the extent possible under law, the author of this file
+   waives all copyright and related or neighboring rights to it.
+*/
+
+(function () {
+    "use strict";
+
+    /** Polyfills and cross-browser fixes */
+
+    if (!Math.sign)
+        Math.sign = function (a) { return a && (a > 0) - (a < 0); };
+
+    if (!String.prototype.repeat)
+        Object.defineProperty(String.prototype, "repeat", {
+            value: function (count) {
+                var string = this.toString();
+                var result = '';
+                var n = count | 0;
+                while (n) {
+                    if (n % 2 === 1)
+                        result += string;
+                    if (n > 1)
+                        string += string;
+                    n >>= 1;
+                }
+                return result;
+            }
+        });
+
+    if (!String.prototype.startsWith)
+        Object.defineProperty(String.prototype, "startsWith", {
+            value: function (sub) {
+                return this.lastIndexOf(sub, 0) !== -1;
+            }
+        });
+
+    if (!String.prototype.endsWith)
+        Object.defineProperty(String.prototype, "endsWith", {
+            value: function (sub) {
+                return this.indexOf(sub, this.length - sub.length) !== -1;
+            }
+        });
+    
+    function toObject (o) {
+        if (o === null || o === undefined)
+            throw new TypeError("invalid ToObject cast");
+        return Object(o);
+    }
+
+    if (!Object.assign)
+        Object.defineProperty(Object, "assign", {
+            value: function (target) {
+                target = toObject(target);
+                for (var i = 1; i < arguments.length; ++i) {
+                    var source = toObject(arguments[i]);
+                    var keys = Object.keys(source);
+                    for (var j = 0; j < keys.length; ++j)
+                        target[keys[j]] = source[keys[j]];
+                }
+            }
+        });
+
+    if (!Array.prototype.fill)
+        Object.defineProperty(Array.prototype, "fill", {
+            value: function (value) {
+                var beg = arguments.length > 1 ? +arguments[1] : 0;
+                var end = arguments.length > 2 ? +arguments[2] : this.length;
+                if (beg < 0) beg += this.length;
+                if (end < 0) end += this.length;
+                for (var i = beg; i < end; ++i)
+                    this[i] = value;
+                return this;
+            }
+        });
+
+    if (typeof window !== "undefined") {
+        window.requestAnimationFrame = (
+            window.requestAnimationFrame
+                || window.mozRequestAnimationFrame
+                || window.webkitRequestAnimationFrame);
+        window.cancelAnimationFrame = (
+            window.cancelAnimationFrame
+                || window.mozCancelAnimationFrame
+                || window.webkitCancelAnimationFrame);
+
+        if (!window.AudioContext)
+            window.AudioContext = (
+                window.webkitAudioContext
+                    || window.mozAudioContext);
+
+        if (window.AudioContext && !window.AudioContext.prototype.createGain)
+            window.AudioContext.prototype.createGain =
+                window.AudioContext.prototype.createGainNode;
+
+        /** Canonicalize fullscreen function names if available.
+
+            Based on http://fullscreen.spec.whatwg.org/, June 7th 2013.
+        */
+
+        if (!Element.prototype.requestFullscreen)
+            Element.prototype.requestFullscreen = (
+                Element.prototype.requestFullScreen
+                    || Element.prototype.webkitRequestFullscreen
+                    || Element.prototype.webkitRequestFullScreen
+                    || Element.prototype.mozRequestFullScreen
+                    || function () {});
+        if (!document.exitFullscreen)
+            document.exitFullscreen = (
+                document.webkitExitFullscreen
+                    || document.webkitCancelFullScreen
+                    || document.mozCancelFullScreen
+                    || function () {});
+        if (!document.hasOwnProperty("fullscreenEnabled"))
+            Object.defineProperty(document, "fullscreenEnabled", {
+                enumerable: true,
+                get: function () {
+                    return (this.webkitFullscreenEnabled
+                            || this.mozFullScreenEnabled
+                            || false);
+                }
+            });
+        if (!document.hasOwnProperty("fullscreenElement"))
+            Object.defineProperty(document, "fullscreenElement", {
+                enumerable: true,
+                get: function () {
+                    return (this.webkitFullscreenElement
+                            || this.webkitCurrentFullScreenElement
+                            || this.mozFullScreenEleement
+                            || null);
+                }
+            });
+    }
+
+    // Check for Promise.all as Chrome 30 shipped an implementation
+    // without it and with some other quirks and we don't want to use
+    // that one.
+    if (typeof Promise === "undefined" || !Promise.all) (function () {
+        /* Polyfill based heavily on Christoph Burgmer's ayepromise
+
+           https://github.com/cburgmer/ayepromise/blob/master/ayepromise.js
+        */
+        /* Wrap an arbitrary number of functions and allow only one of
+           them to be executed and only once */
+        function once () {
+            var wasCalled = false;
+
+            return function (wrappedFunction) {
+                return function () {
+                    if (wasCalled) {
+                        return;
+                    }
+                    wasCalled = true;
+                    wrappedFunction.apply(null, arguments);
+                };
+            };
+        }
+
+        function getThenableIfExists (obj) {
+            // Make sure we only access the accessor once as required by the spec
+            var then = obj && obj.then;
+
+            if (typeof obj === "object" && typeof then === "function")
+                return then.bind(obj);
+        }
+
+        function aThenHandler (onFulfilled, onRejected) {
+            var deferred = defer();
+
+            function doHandlerCall (func, value) {
+                setTimeout(function () {
+                    var returnValue;
+                    try {
+                        returnValue = func(value);
+                    } catch (e) {
+                        deferred.reject(e);
+                        return;
+                    }
+
+                    if (returnValue === deferred.promise) {
+                        deferred.reject(new TypeError());
+                    } else {
+                        deferred.resolve(returnValue);
+                    }
+                }, 0);
+            }
+
+            return {
+                promise: deferred.promise,
+                callFulfilled: function (value) {
+                    if (onFulfilled && onFulfilled.call) {
+                        doHandlerCall(onFulfilled, value);
+                    } else {
+                        deferred.resolve(value);
+                    }
+                },
+                callRejected: function (value) {
+                    if (onRejected && onRejected.call) {
+                        doHandlerCall(onRejected, value);
+                    } else {
+                        deferred.reject(value);
+                    }
+                }
+            };
+        }
+
+        function defer () {
+            // States
+            var PENDING = 0,
+                FULFILLED = 1,
+                REJECTED = 2;
+
+            var state = PENDING,
+                outcome,
+                thenHandlers = [];
+
+            function doFulfill (value) {
+                state = FULFILLED;
+                outcome = value;
+
+                thenHandlers.forEach(function (then) {
+                    then.callFulfilled(outcome);
+                });
+                thenHandlers = null;
+            }
+
+            function doReject (error) {
+                state = REJECTED;
+                outcome = error;
+
+                thenHandlers.forEach(function (then) {
+                    then.callRejected(outcome);
+                });
+                thenHandlers = null;
+            }
+
+            function registerThenHandler (onFulfilled, onRejected) {
+                var thenHandler = aThenHandler(onFulfilled, onRejected);
+
+                if (state === FULFILLED) {
+                    thenHandler.callFulfilled(outcome);
+                } else if (state === REJECTED) {
+                    thenHandler.callRejected(outcome);
+                } else {
+                    thenHandlers.push(thenHandler);
+                }
+
+                return thenHandler.promise;
+            }
+
+            function safelyResolveThenable (thenable) {
+                // Either fulfill, reject or reject with error
+                var onceWrapper = once();
+                try {
+                    thenable(
+                        onceWrapper(transparentlyResolveThenablesAndFulfill),
+                        onceWrapper(doReject)
+                    );
+                } catch (e) {
+                    onceWrapper(doReject)(e);
+                }
+            }
+
+            function transparentlyResolveThenablesAndFulfill (value) {
+                var thenable;
+
+                try {
+                    thenable = getThenableIfExists(value);
+                } catch (e) {
+                    doReject(e);
+                    return;
+                }
+
+                if (thenable) {
+                    safelyResolveThenable(thenable);
+                } else {
+                    doFulfill(value);
+                }
+            }
+
+            var onceWrapper = once();
+            return {
+                resolve: onceWrapper(transparentlyResolveThenablesAndFulfill),
+                reject: onceWrapper(doReject),
+                promise: {
+                    then: registerThenHandler,
+                    "catch": function (onRejected) {
+                        return registerThenHandler(null, onRejected);
+                    }
+                }
+            };
+        }
+
+        function Promise (callback) {
+            var deferred = defer();
+            try {
+                callback(deferred.resolve, deferred.reject);
+            } catch (exc) {
+                deferred.reject(exc);
+            }
+            return deferred.promise;
+        }
+
+        Promise.resolve = function (v) {
+            return new Promise(function (resolve) { resolve(v); });
+        };
+
+        Promise.reject = function (error) {
+            return new Promise(function (_, reject) { reject(error); });
+        };
+
+        Promise.all = function (promises) {
+            return new Promise(function (resolve, reject) {
+                var results = [];
+                var remaining = promises.length;
+                if (remaining === 0)
+                    return resolve([]);
+
+                promises.forEach(function (promise, i) {
+                    var then = getThenableIfExists(promise);
+                    function resolve1 (value) {
+                        results[i] = value;
+                        if (--remaining === 0)
+                            resolve(results);
+                    }
+                    if (then) {
+                        then.call(promise, resolve1, reject);
+                    } else {
+                        --remaining;
+                        results[i] = promise;
+                    }
+                });
+            });
+        };
+
+        this.Promise = Promise;
+    }).call(this);
+}).call(this);
diff --git a/src/yuu/rdr.js b/src/yuu/rdr.js
new file mode 100644 (file)
index 0000000..a03cbec
--- /dev/null
@@ -0,0 +1,426 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (yuu) {
+    "use strict";
+
+    var yT = this.yT || require("./yT");
+    var yf = this.yf || require("./yf");
+
+    if (!yuu.C) require("./ce");
+    if (!yuu.Material) require("./gfx");
+
+    yuu.Renderable = yT({
+        constructor: function (vbuf, primitive, material, uniforms, z) {
+            this.vbuf = vbuf;
+            this.primitive = primitive || yuu.gl.TRIANGLES;
+            this.material = material || yuu.Material.DEFAULT;
+            this.uniforms = uniforms || {};
+            this.z = z || 0.0;
+        },
+
+        bind: function () {
+            this.material.program.setUniforms(this.uniforms);
+            this.vbuf.bindBuffer();
+            this.material.program.setAttribPointers(this.vbuf);
+        },
+
+        draw: function () {
+            this.bind();
+            yuu.gl.drawArrays(this.primitive, 0, this.vbuf.vertexCount);
+        },
+
+        vertexCount: { alias: "vbuf.vertexCount" }
+    });
+
+    yuu.IndexedRenderable = yT(yuu.Renderable, {
+        constructor: function (vbuf, primitive, material, uniforms, z, ibuf) {
+            yuu.Renderable.call(this, vbuf, primitive, material, uniforms, z);
+            this.ibuf = ibuf;
+        },
+
+        bind: function () {
+            yuu.Renderable.prototype.bind.call(this);
+            this.ibuf.bindBuffer();
+        },
+
+        draw: function () {
+            this.bind();
+            yuu.gl.drawElements(
+                this.primitive, this.ibuf.length, this.ibuf.GL_TYPE, 0);
+        },
+
+        vertexCount: {
+            get: function () {
+                return this.ibuf.length;
+            },
+            set: function (vertexCount) {
+                this.vbuf.vertexCount = vertexCount;
+                this.ibuf.maxIndex = vertexCount;
+            }
+        }
+    });
+
+    yuu.Quad = yT({
+        /** A vertex view containing a 2D quadrilateral
+
+            You probably don't want to use this directly. If you want a
+            simple quad, look at QuadC.
+        */
+        constructor: function (vbuf) {
+            this._vbuf = vbuf;
+            this.anchor = "center";
+            this.position = [0.0, 0.0];
+            this.size = [1.0, 1.0];
+            this.texBounds = [0.0, 0.0, 1.0, 1.0];
+            this.color = [1.0, 1.0, 1.0, 1.0];
+        },
+
+        size: {
+            get: function () {
+                var b = this._vbuf.arrays.position;
+                return [b[6] - b[0], b[4] - b[1]];
+            },
+            set: function(size) {
+                var position = this.position;
+                var b = this._vbuf.arrays.position;
+                b[0] = b[3] = b[1] = b[7] = 0;
+                b[6] = b[9] = size[0];
+                b[4] = b[10] = size[1];
+                this.position = position;
+                this._vbuf.dirty = true;
+            }
+        },
+
+        position: {
+            get: function () {
+                var b = this._vbuf.arrays.position;
+                return yuu.anchorPoint(this.anchor, b[0], b[1], b[6], b[4]);
+            },
+            set: function (position) {
+                var size = this.size;
+                var b = this._vbuf.arrays.position;
+                var bottomLeft = yuu.bottomLeft(
+                    this.anchor, position[0], position[1], size[0], size[1]);
+                b[0] = b[3] = bottomLeft[0];
+                b[1] = b[7] = bottomLeft[1];
+                b[6] = b[9] = bottomLeft[0] + size[0];
+                b[4] = b[10] = bottomLeft[1] + size[1];
+                this._vbuf.dirty = true;
+            }
+        },
+
+        x: { synthetic: "position[0]" },
+        y: { synthetic: "position[1]" },
+
+        // Texture coordinate vertices: 12 +...
+        // 2,3    6,7
+        // 0,1    4,5
+
+        texBounds: {
+            get: function() {
+                var b = this._vbuf.arrays.texCoord;
+                return [b[0], b[1], b[6], b[7]];
+            },
+            set: function (uv0uv1) {
+                var b = this._vbuf.arrays.texCoord;
+                b[0] = b[2] = uv0uv1[0];
+                b[1] = b[5] = uv0uv1[1];
+                b[6] = b[4] = uv0uv1[2];
+                b[3] = b[7] = uv0uv1[3];
+                this._vbuf.dirty = true;
+            }
+        },
+
+        // Color vertices: 20 +...
+        // 4,5,6,7   12,13,14,15
+        // 0,1,2,3   8,9,10,11
+
+        color: {
+            get: function () {
+                var b = this._vbuf.arrays.color;
+                return [b[0], b[1], b[2], b[3]];
+            },
+            set: function (rgba) {
+                var b = this._vbuf.arrays.color;
+                var a = rgba[3];
+                b[0] = b[4] = b[8] = b[12] = rgba[0];
+                b[1] = b[5] = b[9] = b[13] = rgba[1];
+                b[2] = b[6] = b[10] = b[14] = rgba[2];
+                if (a !== undefined)
+                    b[3] = b[7] = b[11] = b[15] = a;
+                this._vbuf.dirty = true;
+            }
+        },
+
+        luminance: {
+            get: function () {
+                var color = this.color;
+                return 0.2126 * color[0]
+                    + 0.7152 * color[1]
+                    + 0.0722 * color[2];
+            },
+
+            set: function (v) {
+                this.color = [v, v, v];
+            }
+        },
+
+        alpha: {
+            get: function () { return this._vbuf.arrays.color[3]; },
+            set: function (a) {
+                var b = this._vbuf.arrays.color;
+                b[3] = b[7] = b[11] = b[15] = a;
+                this._vbuf.dirty = true;
+            }
+        }
+    });
+
+    yuu.QuadBatch = yT({
+        constructor: function (capacity) {
+            this.vbuf = new yuu.VertexBuffer(yuu.V3T2C4_F, capacity * 4);
+            this.ibuf = new yuu.IndexBuffer(
+                this.vbuf.vertexCount, capacity * 6);
+            this._capacity = capacity;
+            this._resetAllocations();
+        },
+
+        _vbufSlotFromQuad: function (quad) {
+            if (quad._vbuf.arrays.position.buffer !== this.vbuf.buffer)
+                throw new Error("invalid quad buffer");
+            var offset = quad._vbuf.arrays.position.byteOffset;
+            var bytesPerQuad = (
+                this.vbuf.spec.attribs.position.View.BYTES_PER_ELEMENT
+                    * this.vbuf.spec.attribs.position.elements
+                    * 4 /* vertices per quad */);
+            return offset / bytesPerQuad;
+        },
+        
+        createQuad: function () {
+            var slot = this._freeVbufSlots[this._allocated];
+            if (slot === undefined)
+                throw new Error("out of batch slots");
+            var subdata = this.vbuf.subdata(slot * 4, 4);
+            var index = this._allocated++;
+            var n = 6 * index;
+            this.ibuf.buffer[n + 0] = slot * 4 + 0;
+            this.ibuf.buffer[n + 1] = slot * 4 + 1;
+            this.ibuf.buffer[n + 2] = slot * 4 + 2;
+            this.ibuf.buffer[n + 3] = slot * 4 + 2;
+            this.ibuf.buffer[n + 4] = slot * 4 + 1;
+            this.ibuf.buffer[n + 5] = slot * 4 + 3;
+            this.ibuf.length += 6;
+            this.ibuf.dirty = true;
+            this._vbufToIndex[slot] = index;
+            return new yuu.Quad(subdata);
+        },
+
+        disposeQuad: function (quad) {
+            var slot = this._vbufSlotFromQuad(quad);
+            var index = this._vbufToIndex[slot];
+            this._allocated--;
+            if (index !== this._allocated) {
+                // Unless this was the last index, swap the last index
+                // into the new hole.
+                var n = 6 /* indices per quad */ * index;
+                var m = 6 /* indices per quad */ * this._allocated;
+                var b = this.ibuf.buffer;
+                var lastVbufSlot = b[m] / 4 /* vertices per quad */;
+                if (this._vbufToIndex[lastVbufSlot] !== this._allocated)
+                    throw new Error("allocation index mismatch");
+                b[n + 0] = b[m + 0];
+                b[n + 1] = b[m + 1];
+                b[n + 2] = b[m + 2];
+                b[n + 3] = b[m + 3];
+                b[n + 4] = b[m + 4];
+                b[n + 5] = b[m + 5];
+                this.ibuf.dirty = true;
+                this._vbufToIndex[lastVbufSlot] = index;
+            }
+            this._freeVbufSlots[this._allocated] = slot;
+            this.ibuf.length -= 6;
+        },
+
+        _resetAllocations: function () {
+            this.ibuf.length = 0;
+            var Array = yuu.IndexBuffer.Array(this._capacity);
+            this._freeVbufSlots = new Array(this._capacity);
+            yf.transform(yf.counter(), this._freeVbufSlots);
+            this._allocated = 0;
+            this._vbufToIndex = new Array(this._capacity);
+        },
+
+        disposeAll: function () {
+            this._resetAllocations();
+        }
+    });
+
+    yuu.QuadC = yT(yuu.C, {
+        /** A 2D quadrilateral that tracks the entity's transform
+            
+            By default, the extents of this quad are [-0.5, -0.5] to
+            [0.5, 0.5], and its model matrix is identical to the
+            entity's transform, i.e. it is centered around [0, 0] in
+            the entity's local space, or the entity's nominal location
+            in world space.  This can be changed by adjusting the
+            anchor, position, and size properties.
+        */
+        
+        constructor: function (material) {
+            var buffer = new yuu.VertexBuffer(yuu.V3T2C4_F, 4);
+            this._quad = new yuu.Quad(buffer);
+            this._rdro = new yuu.Renderable(
+                buffer, yuu.gl.TRIANGLE_STRIP, material,
+                { model: mat4.create() }, 0.0);
+        },
+
+        TAPS: ["queueRenderables"],
+
+        queueRenderables: function (rdros) {
+            mat4.copy(this._rdro.uniforms.model,
+                      this.entity.transform.matrix);
+            rdros.push(this._rdro);
+        },
+
+        // TODO: yT should offer some way to specify these in two
+        // lists, i.e. the rdro aliases, and the quad aliases.
+
+        material: { alias: "_rdro.material", chainable: true },
+        z: { alias: "_rdro.z", chainable: true },
+        uniforms: { alias: "_rdro.uniforms" },
+        size: { alias: "_quad.size", chainable: true },
+        position: { alias: "_quad.position", chainable: true },
+        anchor: { alias: "_quad.anchor", chainable: true },
+        xy: { alias: "_quad.position", chainable: true },
+        texBounds: { alias: "_quad.texBounds", chainable: true },
+        color: { alias: "_quad.color", chainable: true },
+        alpha: { alias: "_quad.alpha", chainable: true },
+        luminance: { alias: "_quad.luminance", chainable: true },
+    });
+
+    yuu.QuadBatchC = yT(yuu.C, {
+        /** A 2D quadrilateral batch that tracks the entity's transform
+            
+         */
+        
+        constructor: function (capacity, material) {
+            this._batch = new yuu.QuadBatch(capacity);
+            this._rdro = new yuu.IndexedRenderable(
+                this._batch.vbuf, yuu.gl.TRIANGLES, material,
+                { model: mat4.create() }, 0.0, this._batch.ibuf);
+        },
+
+        TAPS: ["queueRenderables"],
+
+        queueRenderables: function (rdros) {
+            mat4.copy(this._rdro.uniforms.model,
+                      this.entity.transform.matrix);
+            rdros.push(this._rdro);
+        },
+
+        material: { alias: "_rdro.material", chainable: true },
+        z: { alias: "_rdro.z", chainable: true },
+        uniforms: { alias: "_rdro.uniforms" },
+        createQuad: { proxy: "_batch.createQuad" },
+        disposeQuad: { proxy: "_batch.disposeQuad" },
+        disposeAll: { proxy: "_batch.disposeAll" },
+    });
+
+    function sortRenderables(a, b) { return a.z - b.z; }
+
+    yuu.Layer = yT({
+        /** List of renderables and per-layer uniforms
+
+            These uniforms usually include the projection and view
+            matrices, set to a [-1, 1] orthographic projection and the
+            identity view by default.
+        */
+
+        // TODO: This is a bad design. Too powerful to be efficient or
+        // a straightforward part of Scene; not enough to abstract
+        // hard things like render passes.
+
+        constructor: function () {
+            this.rdros = [];
+            this.uniforms = {
+                projection: mat4.ortho(mat4.create(), -1, 1, -1, 1, -1, 1),
+                view: mat4.create()
+            };
+        },
+
+        worldFromDevice: yf.argcd(
+            function (p) {
+                var t = this.worldFromDevice(p.x || p.pageX || p[0] || 0,
+                                             p.y || p.pageY || p[1] || 0);
+                t.inside = p.inside;
+                return t;
+            },
+            function (x, y) {
+                var p = { 0: x, 1: y };
+                var m = mat4.mul(mat4.create(),
+                                 this.uniforms.projection, this.uniforms.view);
+                m = mat4.invert(m, m);
+                vec2.transformMat4(p, p, m);
+                p.x = p[0]; p.y = p[1];
+                return p;
+            }
+        ),
+
+        worldFromCanvas: yf.argcd(
+            function (p) {
+                return this.worldFromDevice(yuu.deviceFromCanvas(p));
+            },
+            function (x, y) {
+                return this.worldFromDevice(yuu.deviceFromCanvas(x, y));
+            }
+        ),
+
+        resize: function (x, y, w, h) {
+            /** Set a 2D orthographic project with an origin and size
+
+                Arguments:
+                scene.resize(originX, originY, width, height)
+                scene.resize(width, height) // Origin at 0, 0
+                scene.resize(origin, size)
+                scene.resize(size) // Origin at 0, 0
+            */
+            if (y === undefined) {
+                w = x[0]; h = x[1]; x = y = 0;
+            } else if (w === undefined) {
+                if (x.length === undefined) {
+                    w = x; h = y; x = y = 0;
+                } else {
+                    w = y[0]; h = y[1];
+                    y = x[1]; x = x[0];
+                }
+            }
+            mat4.ortho(this.uniforms.projection, x, x + w, y, y + h, -1, 1);
+        },
+
+        render: function () {
+            /** Render all queued renderables */
+            this.rdros.sort(sortRenderables);
+            var mat = null;
+            for (var j = 0; j < this.rdros.length; ++j) {
+                var rdro = this.rdros[j];
+                if (mat !== rdro.material) {
+                    if (mat)
+                        mat.disable();
+                    mat = rdro.material;
+                    rdro.material.enable(this.uniforms);
+                }
+                rdro.draw();
+            }
+        },
+
+        clear: function () {
+            this.rdros.length = 0;
+        }
+    });
+
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined"
+        ? this.yuu : (module.exports = require('./core')));
diff --git a/src/yuu/storage.js b/src/yuu/storage.js
new file mode 100644 (file)
index 0000000..1ad702e
--- /dev/null
@@ -0,0 +1,188 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (exports) {
+    "use strict";
+
+    var yT = this.yT || require('./yT');
+
+    var FakeStorage = exports.FakeStorage = yT({
+        /** Fake, ephemeral storage roughly like Web Storage
+
+            This is the fallback storage when permission is denied or
+            otherwise busted. It just stores a dictionary for as long as
+            the object survives.
+        */
+        constructor: function () {
+            this._storage = {};
+        },
+
+        getItem: function (key) {
+            return (key in this._storage) ? this._storage[key] : null;
+        },
+
+        setItem: function (key, value) {
+            this._storage[key] = value.toString();
+        },
+
+        removeItem: function (key) {
+            delete this._storage[key];
+        },
+
+        clear: function () {
+            this._storage = {};
+        },
+
+        length: {
+            get: function () { return Object.keys(this._storage).length; }
+        },
+
+        key: function (n) {
+            // Object.keys isn't guaranteed to have a consistent order
+            // even when nothing changes, so normalize it by sorting.
+            var keys = Object.keys(this._storage).sort();
+            return (n >= 0 && n < keys.length) ? keys[n] : null;
+        }
+    });
+
+    var PrefixedStorage = exports.PrefixedStorage = yT({
+        /** Per-application storage roughly like Web Storage
+
+            This storage prefixes all keys with a special token, so you
+            can run multiple applications on the same origin without
+            the risk of conflicting keys.
+
+            A caveat of this approach is clear() is not atomic.
+        */
+
+        constructor: function (storage, prefix) {
+            this._storage = storage;
+            this._prefix = prefix + " -- ";
+        },
+
+        _key: function (key) {
+            return this._prefix + key;
+        },
+
+        _unkey: function (key) {
+            return key.substring(this._prefix.length);
+        },
+
+        _iskey: function (key) {
+            return key.startsWith(this._prefix);
+        },
+
+        _keys: function () {
+            var keys = [];
+            var key;
+            var i = 0;
+            while ((key = this._storage.key(i++)) !== null)
+                if (this._iskey(key))
+                    keys.push(this._unkey(key));
+            return keys;
+        },
+
+        getItem: function (key) {
+            return this._storage.getItem(this._key(key));
+        },
+
+        setItem: function (key, value) {
+            return this._storage.setItem(this._key(key), value);
+        },
+
+        removeItem: function (key) {
+            return this._storage.removeItem(this._key(key));
+        },
+
+        clear: function () {
+            this._keys().forEach(this.removeItem, this);
+        },
+
+        length: {
+            get: function () { return this._keys().length; }
+        },
+
+        key: function (n) {
+            var keys = this._keys().sort();
+            return (n >= 0 && n < keys.length) ? keys[n] : null;
+        }
+    });
+
+    var Storage = exports.Storage = yT({
+        /** Higher-level access to Web Storage-esque things
+
+            Storage lets you store and retrieve JSON-serializable
+            objects inside a Web Storage container.
+
+            You can specify default values. If you retrieve an object
+            that hasn't been set, you get its default value.
+
+            Storage automatically falls back to an ephemeral storage
+            backend if a SecurityException occurs during startup.
+        */
+
+        constructor: function (storage, defaults) {
+            this._storage = storage || new FakeStorage();
+            this._defaults = defaults || {};
+
+            try {
+                this.setFlag('__ystorage__');
+            } catch (exc) {
+                this._storage = new FakeStorage();
+                console.error("Unable to use provided storage:", exc);
+            }
+        },
+
+        getObject: function (key, fallbackValue) {
+            var v = this._storage.getItem(key);
+            if (v === null) {
+                return (key in this._defaults)
+                    ? this._defaults[key]
+                    : fallbackValue;
+            }
+            try {
+                return JSON.parse(v);
+            } catch (exc) {
+                console.error("Malformed storage value:", key, v, exc);
+                return (key in this._defaults)
+                    ? this._defaults[key]
+                    : fallbackValue;
+            }
+        },
+
+        setObject: function (key, value) {
+            this._storage.setItem(key, JSON.stringify(value));
+        },
+
+        removeObject: { proxy: '_storage.removeItem' },
+
+        getFlag: function (key) {
+            return !!this.getObject(key, false);
+        },
+
+        setFlag: function (key) {
+            return this.setObject(key, true);
+        },
+
+        clearFlag: function (key) {
+            return this.setObject(key, false);
+        },
+
+        clear: { proxy: '_storage.clear' }
+    });
+
+    exports.getStorage = function (prefix, defaults, backend) {
+        /** Create a Storage with prefixed access to localStorage. */
+        prefix = prefix
+            || (document &&
+                (document.documentElement.getAttribute('data-appid')
+                 || document.title));
+        backend = backend || localStorage;
+        return new Storage(new PrefixedStorage(backend, prefix), defaults);
+    };
+
+}).call(typeof exports === 'undefined' ? this : exports,
+        typeof exports === 'undefined' ? (this.ystorage = {}) : exports);
diff --git a/src/yuu/yT.js b/src/yuu/yT.js
new file mode 100644 (file)
index 0000000..0c86c89
--- /dev/null
@@ -0,0 +1,364 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function (module) {
+    "use strict";
+
+    /** yT - yuu type creation
+
+        yT is a function like `Object.create`, but with support for
+        more powerful property descriptors (referred to as _extended
+        property descriptors (XPDs)_). Most standard JavaScript
+        property descriptors are valid XPDs, but XPDs allow shortcuts
+        to specify common descriptor patterns.
+
+        Equivalents for `Object.defineProperties` and
+        `Object.defineProperty` are also provided.
+
+        ## Extended Property Descriptors
+
+        Any standard descriptor that has a `get` function (called an
+        'accessor descriptor') or a `value` property (called a 'data
+        descriptor') is also valid XPD.
+
+        An extended descriptor that does not have either of these and
+        does not meet any of the other conditions below is equivalent
+        to a data descriptor with a `value` of itself, referred to as
+        a 'bare value descriptor'. For example, the following two XPDs
+        are equivalent:
+
+            { x: 1 }                { x: { value: 1 } }
+
+        (This means a useless descriptor like `{}` is interpreted as a
+        data descriptor with value `undefined` by `Object.create` but
+        a bare value descriptor with value `{}` by `yT`.)
+        
+        In addition, extended descriptors have several other formats
+        which can be used to generate different kinds of idomatic
+        accessors:
+
+         * `alias` - This property is a synonym for a different property.
+            Reads and writes to it will be mapped to reads and writes
+            to the aliased property. 
+                { firstChild: { alias: "children[0]" } }
+            is equivalent to
+                { firstChild: {
+                    get: function () { return this.children[0]; },
+                    set: function (v) { this.children[0] = v; }
+                } }
+
+         * `proxy` - This property is a synonym for a method call
+            on a different object.
+                { start: { proxy: "engine.start" } }
+            is equivalent to
+                { start: { value: function () {
+                    return this.engine.start.apply(this.engine, arguments);
+                } } }
+
+            Using `alias` rather than `proxy` would result in the
+            wrong (non-engine) `this` argument being passed to the
+            `start` method.
+
+         * `aliasSynthetic` - Aliases don't work if one of the
+            properties in the lookup chain is a temporary variable.
+            For example, aliasing `x` to `position[0]` is no good if
+            `position` itself has a getter like `transform.slice(12)`
+            because the assignment to the returned value will have
+            no effect.
+
+            `aliasSynthetic` can be used to capture the temporary,
+            assign to it, and then assign the whole temporary back.
+                { x: { aliasSynthetic: "position[0]" } }
+            Generates the same `get` as `alias`, but `set` is
+                function (v) {
+                    var t = this.position;
+                    t[0] = v;
+                    this.position = t;
+                }
+
+            `aliasSynthetic` assumes the next-to-last value is the
+            temporary, e.g. in `a.b.c.d`, `a.b.c` is the temporary.
+            If rather e.g. `a.b` is the temporary, you can separate
+            the `alias` and `synthetic` parts:
+                { x: { alias: "a.b.c.d", synthetic: "a.b" } }
+
+         * `swizzle` - Swizzling lets you treat separate properties
+            as one array property. For example if you have a color
+            class with individual r, g, and b properties,
+                { rgb: { swizzle: ["r", "g", "b"] } }
+            is equivalent to
+                { rgb: {
+                    get: function () { return [this.r, this.g, this.b] },
+                    set: function (v) {
+                        this.r = v[0];
+                        this.g = v[1];
+                        this.b = v[2];
+                    }
+                } }
+
+        Any descriptor may also have the `chainable` property set,
+        which generates a chainable setter function. Chainable data
+        descriptors are writable by default.
+            { x: { value: 0, chainable: true } }
+        is equivalent to
+            { x: { value: 0, writable: true },
+              setX: { value: function (v) { this.x = v; return this; } }
+            }
+
+        ## Example
+        
+        An example of a simple 2D Point class using XPDs:
+
+            var Point = yT(Object, {
+                constructor: function (x, y) {
+                    this.x = x || 0;
+                    this.y = y || 0;
+                },
+
+                0: { alias: "x" },
+                1: { alias: "y" },
+                xy: { swizzle: "xy" },
+                yx: { swizzle: "yx" },
+
+                angle: {
+                    chainable: true,
+                    get: function () {
+                        return Math.atan2(this.y, this.x);
+                    },
+                    set: function (angle) {
+                        var magnitude = this.magnitude;
+                        this.x = Math.cos(angle) * magnitude;
+                        this.y = Math.sin(angle) * magnitude;
+                    }
+                },
+
+                magnitude: {
+                    chainable: true,
+                    get: function () {
+                        return Math.sqrt(this.x * this.x + this.y * this.y);
+                    },
+                    set: function (magnitude) {
+                        var angle = this.angle;
+                        this.x = Math.cos(angle) * magnitude;
+                        this.y = Math.sin(angle) * magnitude;
+                    }
+                },
+
+                length: 2
+            });
+
+            var p = new Point(3, 0);
+            p[0] === 3; // true
+            p.y = 4; p[1] == 4; // true
+            p.magnitude = 1; // normalize
+            p.xy = p.yx; // transpose
+
+            new Point().setMagnitude(m).setAngle(a);
+                // construct a point from an angle and magnitude in a
+                // single step.
+     */
+
+    /* jshint -W054 */ // Function constructors are the whole point here.
+
+    function isFunction (o) {
+        /** Check if a value is a function */
+        return Object.prototype.toString.call(o) === '[object Function]';
+    }
+
+    function update (dst, src) {
+        /** Copy every enumerable key and its value from src to dst */
+        for (var k in src)
+            dst[k] = src[k];
+        return dst;
+    }
+
+    function rooted (path) {
+        if (typeof path === "number")
+            return "[" + path + "]";
+        else if (path[0] !== "." && path[0] !== "[")
+            return "." + path;
+        else
+            return path;
+    }
+
+    function chainableName (name) {
+        return "set" + name[0].toUpperCase() + name.substring(1);
+    }
+
+    function chainableSetter (name) {
+        return new Function(
+            "value",
+            "this" + rooted(name) + " = value; " + "return this;");
+    }
+
+    function alias (path, readonly) {
+        path = rooted(path);
+        return readonly
+            ? { get: new Function("return this" + path + ";") }
+            : { get: new Function("return this" + path + ";"),
+                set: new Function("v", "return this" + path + " = v;") };
+    }
+
+    function proxy (path) {
+        path = rooted(path);
+        var prop = path.substr(0, path.lastIndexOf("."));
+        return {
+            value: new Function(
+                "return this" + path + ".apply(this" + prop + ", arguments);")
+        };
+    }
+
+    function swizzle (props) {
+        props = Array.prototype.map.call(props, function (p) {
+            return "this" + rooted(p);
+        });
+        var assignments = props.map(function (p, i) {
+            return p + " = x[" + i + "];";
+        });
+        return {
+            get: new Function("return [" + props.join(", ") + "];"),
+            set: new Function("x", assignments.join("\n"))
+        };
+    }
+
+    function aliasSynthetic (path) {
+        var idx = Math.max(path.lastIndexOf("."), path.lastIndexOf("["));
+        return synthetic(path.substring(0, idx), path);
+    }
+
+    function synthetic (synthPath, path) {
+        synthPath = rooted(synthPath);
+        path = rooted(path).substring(synthPath.length);
+        return {
+            get: new Function("return this" + synthPath + path + ";"),
+            set: new Function("v",
+                              ["var t = this" + synthPath + ";",
+                               "t" + path + " = v; ",
+                               "this" + synthPath + " = t;",
+                               ].join("\n"))
+        };
+    }
+
+    function isAccessorDescriptor (pd) {
+        /** Check if `pd` is a descriptor describing an accessor */
+        return pd && typeof pd === "object" && isFunction(pd.get);
+    }
+
+    function isDataDescriptor (pd) {
+        /** Check if `pd` is a descriptor describing data */
+        return pd && typeof pd === "object" && ("value" in pd);
+    }
+    
+    function isDescriptor (pd) {
+        /** Check if `pd` is any kind of descriptor */
+        return isDataDescriptor(pd) || isAccessorDescriptor(pd);
+    }
+
+    function createDescriptors (name, xpd, pds) {
+        if (xpd.alias !== undefined) {
+            if (xpd.synthetic !== undefined)
+                pds[name] = update(xpd, synthetic(xpd.alias, xpd.synthetic));
+            else
+                pds[name] = update(xpd, alias(xpd.alias, xpd.readonly));
+        } else if (xpd.proxy !== undefined) {
+            pds[name] = update(xpd, proxy(xpd.proxy));
+        } else if (xpd.swizzle !== undefined) {
+            pds[name] = update(xpd, swizzle(xpd.swizzle));
+        } else if (xpd.aliasSynthetic !== undefined) {
+            pds[name] = update(xpd, aliasSynthetic(xpd.aliasSynthetic));
+        }
+
+        pds[name] = isDescriptor(xpd) ? xpd : { value: xpd };
+        if (xpd.chainable) {
+            pds[chainableName(name)] = { value: chainableSetter(name) };
+            if (isDataDescriptor(pds[name]) && !("writable" in pds[name]))
+                pds[name].writable = true;
+        }
+    }
+
+    function xpdsToPds (xpds) {
+        var pds = {};
+        Object.keys(xpds).forEach(function (k) {
+            createDescriptors(k, xpds[k], pds);
+        });
+        return pds;
+    }
+
+    function T (parent, xpds) {
+        /** Create a new class described by XPDs
+
+            This function is similar to `Object.create` but supports
+            extended property descriptors as described above.
+
+            yT(parent, map of extended descriptors)
+            yT(map of extended descriptors)
+                // The parent is assumed to be Object
+
+            This returns a _function_ which acts as a constructor for
+            the provided type. If there is a property descriptor named
+            `constructor` it is returned; otherwise a new function
+            that calls the parent's.
+
+            `parent` may be either the parent constructor or a
+            prototype.
+        */
+        if (!xpds) {
+            xpds = parent;
+            parent = Object;
+        }
+        parent = isFunction(parent) && parent.prototype || parent;
+        var pds = xpdsToPds(xpds);
+        var ctor = pds.constructor && pds.constructor.value;
+        if (!ctor || ctor === {}.constructor) {
+            ctor = parent
+                ? function () { parent.constructor.apply(this, arguments); }
+                : function () { };
+            pds.constructor = { value: ctor };
+        }
+        ctor.prototype = Object.create(parent, pds);
+        return ctor;
+    }
+
+    T.defineProperties = function (o, xpds) {
+        /** Add properties described by XPDs to an object
+
+            This function is similar to`Object.defineProperties`,
+            but supports the same features as `yT`.
+        */
+        return Object.defineProperties(o, xpdsToPds(xpds));
+    };
+
+    T.defineProperty = function (o, name, xpd) {
+        /** Add a property described by an XPD to an object
+
+            This function is similar to`Object.defineProperty`,
+            but supports the same features as `yT`.
+        */
+        var xpds = {};
+        xpds[name] = xpd;
+        return T.defineProperties(o, xpds);
+    };
+
+    T.getPropertyDescriptor = function (o, name) {
+        /** Look up a descriptor from `o`'s prototype chain */
+        var v = null;
+        while (!v && o) {
+            v = Object.getOwnPropertyDescriptor(o, name);
+            o = Object.getPrototypeOf(o);
+        }
+        return v;
+    };
+
+    T.isAccessorDescriptor = isAccessorDescriptor;
+    T.isDataDescriptor = isDataDescriptor;
+    T.isDescriptor = isDescriptor;
+
+    if (module)
+        module.exports = T;
+    else
+        this.yT = T;
+}).call(typeof exports === "undefined" ? this : exports,
+        typeof exports === "undefined" ? null : module);
diff --git a/src/yuu/yf.js b/src/yuu/yf.js
new file mode 100644 (file)
index 0000000..5df966d
--- /dev/null
@@ -0,0 +1,578 @@
+/* Copyright 2014 Yukkuri Games
+   Licensed under the terms of the GNU GPL v2 or later
+   @license http://www.gnu.org/licenses/gpl-2.0.html
+   @source: http://yukkurigames.com/yuu/
+*/
+
+(function () {
+    "use strict";
+
+    /** yf - yuu foundation
+
+        A collection of tools for functional programming and the kind
+        of sequence (array) manipulations usually associated with
+        functional programming.
+
+        yf doesn't depend on yuu specifically, but may use standard
+        features not yet widely supported provided by yuu/pre.
+     */
+
+    function isFunction (o) {
+        /** Check if a value is a function */
+        return Object.prototype.toString.call(o) === '[object Function]';
+    }
+
+    function isString (s) {
+        /** Check if a value is a string */
+        return Object.prototype.toString.call(s) === "[object String]";
+    }
+
+    function I (x) { return x; }
+
+    function K (x) { return function () { return x; }; }
+
+    /** Get the minimum or maximum of two objects
+
+        The default JavaScript Math.min and Math.max are unsuitable as
+        generic functions. They don't work on strings. Because they
+        support a variable numbers of arguments they don't work
+        correctly when passed directly to higher-order functions that
+        pass "bonus" arguments such as Array.prototype.reduce.
+
+        These functions work on any objects comparable with < and >,
+        and only consider two arguments.
+    */
+    function min (a, b) { return b < a ? b : a; }
+    function max (a, b) { return b > a ? b : a; }
+    function clamp(x, lo, hi) { return max(lo, min(hi, x)); }
+
+    /** Get a property from the this object by name */
+    function getter (key) { return this[key]; }
+
+    /** Set a property value on the this object by name */
+    function setter (key, value) { this[key] = value; }
+
+    function unbind (f) {
+        /** Factor out the special 'this' argument from a function
+
+            unbind(f)(a, b, c) is equivalent to f.call(a, b, c), and
+            you may store the return value for later use without
+            storing the original function.
+        */
+
+        // return f.call.bind(f) is ~15% slower than this closure on
+        // Chrome 31 and negligibly slower on Firefox 25.
+        return function () {
+            return f.call.apply(f, arguments);
+        };
+    }
+
+    /** Unbound versions of useful functions */
+    var slice = unbind(Array.prototype.slice);
+
+    function contains (seq, o) {
+        return Array.prototype.indexOf.call(seq, o) !== -1;
+    }
+
+    function lacks (seq, o) {
+        return !contains(seq, o);
+    }
+
+    /** The first element of a sequence */
+    function head (a) { return a[0]; }
+
+    /** All but the first element of a sequence */
+    function tail (a) { return slice(a, 1); }
+
+    /** The last element of a sequence */
+    function last (a) { return a[a.length - 1]; }
+
+    /** All but the last element of a sequence */
+    function most (a) { return slice(a, 0, -1); }
+
+    /** The length of a sequence */
+    function len (seq) { return seq.length; }
+
+    /** Note that yf's argument order is slightly different from
+        JavaScript's normal order (but the same as most other
+        languages with functional elements).
+
+        This is irrelevant for `fold` and `filter`, but `each` and
+        `map` can take multiple sequences to allow callback functions
+        with arbitrary arity.
+
+        Because of this difference, there is no final `thisArg` as in
+        e.g. the standard Array's forEach. Instead the `this` of
+        the original call is forwarded. For example,
+            someArray.forEach(callback, thisArg)
+        becomes
+            yf.each.call(thisArg, callback, someArray)
+    */
+
+    function foldl (callback, seq, value) {
+        /** Like Array's reduce, but with no extra arguments
+
+            This traverses the sequence in indexed order, low-to-high,
+            and calls the provided function with the accumulated value
+            and the sequence item.
+        */
+        var i = 0;
+        if (arguments.length === 2)
+            value = seq[i++];
+        for (; i < seq.length; ++i)
+            value = callback.call(this, value, seq[i]);
+        return value;
+    }
+
+    function foldr (callback, seq, value) {
+        /** Like foldl, but reversed.
+
+            This traverses the sequence in indexed order, high-to-low,
+            and calls the provided function with the sequence item and
+            the accumulated value.
+        */
+        var i = seq.length - 1;
+        if (arguments.length === 2)
+            value = seq[i--];
+        for (; i >= 0; --i)
+            value = callback.call(this, seq[i], value);
+        return value;
+    }
+
+    function call (f, x) { return f.call(this, x); }
+
+    function compose () {
+        /** Create a function by composing multiple functions
+
+            compose(f, g, h)(x, y, z) is equivalent to f(g(h(x, y, z))).
+        */
+        var inner = last(arguments);
+        var funcs = most(arguments);
+        return function () {
+            return foldr.call(this, call, funcs, inner.apply(this, arguments));
+        };
+    }
+
+    // Generating specialized functions for plucking the nth argument
+    // and passing it to a callback is 100-120x faster than the
+    // generic one in V8.
+    //
+    // So generate specialized functions for small argc, and only
+    // resort to the generic approach when there's many sequences.
+    var CALLS = [];
+
+    /* jshint -W054*/ /* Function constructor is a form of eval */
+    for (var args = []; args.length < 32;
+         args.push(", seqs[" + args.length + "][i]"))
+        CALLS[args.length] = new Function(
+            "callback", "i", "seqs",
+            "return callback.call(this" + args.join("") + ");");
+
+    function lookup (prop) {
+        /** Return a function that gets a particular property */
+
+        // Building this as a closure is ~2-5x faster than writing
+        // a function lookup(prop, o) and binding it.
+        return function (s) { return s[prop]; };
+    }
+
+    function callx (callback, i, seqs) {
+        return callback.apply(this, seqs.map(lookup(i)));
+    }
+
+    function calln (argc) {
+        return CALLS[argc] || callx;
+    }
+
+    function argv (f) {
+        /** Provide a variadic-like version of f
+
+            Arguments passed to the function including and after the
+            final named one are collected and passed as a single
+            sequence (not necessarily Array) value.
+        */
+        var length = f.length;
+        switch (length) {
+        case 0: throw new Error("don't use argv for 0-ary functions");
+        case 1: return function () { return f.call(this, arguments); };
+        default:
+            return function () {
+                arguments[length - 1] = slice(arguments, length - 1);
+                return f.apply(this, arguments);
+            };
+        }
+    }
+
+    var each = argv(function (callback, seqs) {
+        /** Call a function on the given sequences' elements
+
+            If one sequence is longer than the others, `undefined`
+            will be passed for that sequence's argument.
+
+            For example, each(f, [a, b, c], [x, y]) is equivalent to
+                f(a, x);
+                f(b, y);
+                f(c, undefined);
+
+            If you want to pass a thisArg to the callback, use
+            each.call(thisArg, callback, ...).
+        */
+        var length = Math.max.apply(Math, seqs.map(len));
+        var fn = calln(seqs.length);
+        for (var i = 0; i < length; ++i)
+            fn.call(this, callback, i, seqs);
+    });
+
+    var eachr = argv(function (callback, seqs) {
+        /** Call a function on the given sequences' elements, backwards
+            
+            If one sequence is longer than the others, `undefined` will be
+            passed for that sequence's argument.
+
+            For example, each(f, [a, b, c], [x, y]) is equivalent to
+                f(c, undefined);
+                f(b, y);
+                f(a, x);
+
+            If you want to pass a thisArg to the callback, use
+            eachr.call(thisArg, callback, ...).
+        */
+        var length = Math.max.apply(Math, seqs.map(len));
+        var fn = calln(seqs.length);
+        for (var i = length - 1; i >= 0; --i)
+            fn.call(this, callback, i, seqs);
+    });
+
+    function pusher (a) {
+        // Ridiculously faster than a.push.bind(a). :(
+        return function (o) { a.push(o); };
+    }
+
+    function map () {
+        /** Build an array by applying a function to sequences
+
+            Similar to Array.prototype.map, but allows passing multiple
+            sequences to call functions of any arity, as with each.
+
+            If you want to pass a thisArg to the callback, use
+            map.call(thisArg, callback, ...).
+        */
+        var a = [];
+        arguments[0] = compose(pusher(a), arguments[0]);
+        each.apply(this, arguments);
+        return a;
+    }
+
+    function mapr () {
+        /** Build an array by applying a function to sequences backwards
+
+            As eachr is to each, so mapr is to map.
+        */
+        var a = [];
+        arguments[0] = compose(pusher(a), arguments[0]);
+        eachr.apply(this, arguments);
+        return a;
+    }
+
+    function filter (callback, seq) {
+        /** Build an array without the elements that fail the predicate
+
+            Like Array.prototype.filter, but if null is passed for the
+            predicate, any false-y values will be removed.
+        */
+        var a = [];
+        callback = callback || I;
+        function _ (o) { if (callback.call(this, o)) a.push(o); }
+        each.call(this, _, seq);
+        return a;
+    }
+
+    function packed (f) {
+        /** Pack arguments to a function into an Array
+
+            packed(f)([a, b, c]) is equivalent to f(a, b, c).
+        */
+        return function (args) { return f.apply(this, args); };
+    }
+
+    function argcd () {
+        /** Create a function that dispatches based on argument count
+
+            Pass a list of functions with varying argument lengths, e.g.
+                var f = argcd(function () { ... },
+                              function (a, b) { ... },
+                              function (a, b, c) { ... });
+
+            This can be used for function overloading, clearer
+            defaults for positional parameters, or significantly more
+            evil things.
+
+            The functions can also be accessed directly by their
+            argument count, e.g. f[0], f[2], f[3] above.
+
+            If no matching argument count is found for a call, a
+            TypeError is raised.
+        */
+        if (arguments.length === 1)
+            throw new Error("don't use argcd for one function");
+        var table = (function table () {
+            return table[arguments.length].apply(this, arguments);
+        });
+        for (var i = 0; i < arguments.length; ++i)
+            table[arguments[i].length] = arguments[i];
+        return table;
+    }
+
+    var irange = argcd(
+        /** Call the provided callback counting as it goes
+            
+            irange(x) counts from [0, x), by 1.
+            
+            irange(x, y) counts from [x, y), by 1.
+            
+            irange(x, y, s) counts from [x, y), by s.
+        */
+        function (callback, stop) {
+            return irange.call(this, callback, 0, stop, 1);
+        },
+        function (callback, start, stop) {
+            return irange.call(this, callback, start, stop, 1);
+        },
+        function (callback, start, stop, step) {
+            var length = Math.max(Math.ceil((stop - start) / step), 0);
+            for (var i = 0; i < length; ++i)
+                callback.call(this, start + step * i);
+        }
+    );
+
+    function capture1 (ifunc) {
+        /** Build an array from the values of an iterating function
+
+            The elements added to the array are the first arguments
+            the iterating function passes.
+        */
+        return function () {
+            var a = [];
+            var args = slice(arguments);
+            args.unshift(pusher(a));
+            ifunc.apply(this, args);
+            return a;
+        };
+    }
+
+    /** Generate an Array from range of numbers, as irange */
+    var range = capture1(irange);
+
+    function ipairs (callback, o) {
+        /** Call the provided callback with each key and value of the object */
+        each.call(this, function (k) { callback.call(this, k, o[k]); },
+                  Object.keys(o));
+    }
+
+    function iprototypeChain (callback, o) {
+        /** Traverse an object and its prototype chain */
+        do {
+            callback.call(this, o);
+        } while ((o = Object.getPrototypeOf(o)));
+    }
+
+    function allKeys (o) {
+        /** Get *ALL* property names. Even unowned non-enumerable ones. */
+        var props = [];
+        iprototypeChain(compose(packed(props.push).bind(props),
+                                Object.getOwnPropertyNames),
+                        o);
+        return props.sort().filter(function (p, i, a) {
+            return p !== a[i - 1];
+        });
+    }
+
+    var some = argv(function (callback, seqs) {
+        callback = callback || I;
+        var length = Math.max.apply(Math, seqs.map(len));
+        var fn = calln(seqs.length);
+        for (var i = 0; i < length; ++i)
+            if (fn.call(this, callback, i, seqs))
+                return true;
+        return false;
+    });
+
+    var eachrUntil = argv(function (callback, seqs) { // Reverse of `some`
+        callback = callback || I;
+        var length = Math.max.apply(Math, seqs.map(len));
+        var fn = calln(seqs.length);
+        for (var i = length - 1; i >= 0; --i)
+            if (fn.call(this, callback, i, seqs))
+                return true;
+        return false;
+    });
+
+    function not (f) {
+        /** Build a function equivalent to !f(...) */
+        return function () { return !f.apply(this, arguments); };
+    }
+
+    function every () {
+        arguments[0] = not(arguments[0] || I);
+        return !some.apply(this, arguments);
+    }
+
+    function none () {
+        return !some.apply(this, arguments);
+    }
+
+    function repeat (o, n) {
+        return (new Array(n)).fill(o);
+    }
+
+    function arrayify (o) {
+        /** Equivalent to [].concat(o) but faster */
+        return Array.isArray(o) ? o : [o];
+    }
+
+    function stash (key, value, a) {
+        each(function (o) { o[key] = value; }, a);
+    }
+
+    function without (seq, o) {
+        if (arguments.length === 2)
+            return filter(function (a) { return a !== o; }, seq);
+        var os = slice(arguments, 1);
+        return filter(lacks.bind(null, os), seq);
+    }
+
+    function construct (ctr, args) {
+        args = slice(args);
+        args.unshift(ctr);
+        return new (Function.prototype.bind.apply(ctr, args))();
+    }
+
+    function new_ (ctr) {
+        return function () { return construct(ctr, arguments); };
+    }
+
+    function counter (start) {
+        var i = +(start || 0);
+        return function () { return i++; };
+    }
+
+    function volatile (f) {
+        return { valueOf: f };
+    }
+
+    function transform (callback, seq) {
+        irange.call(this, function (i) {
+            seq[i] = callback.call(this, seq[i]);
+        }, seq.length);
+    }
+
+    function mapValues (callback, o) {
+        var r = {};
+        for (var k in o)
+            r[k] = callback.call(this, o[k]);
+        return r;
+    }
+
+    function insertBefore (seq, o, before) {
+        var idx = seq.lastIndexOf(before);
+        if (idx === -1)
+            seq.push(o);
+        else
+            seq.splice(idx, 0, o);
+        return seq;
+    }
+
+    function seqEqual (a, b) {
+        if (a.length !== b.length)
+            return false;
+        for (var i = 0; i < a.length; ++i)
+            if (a[i] !== b[i])
+                return false;
+        return true;
+    }
+
+    function debounce (callback, wait) {
+        wait = wait || 100;
+        var this_ = null;
+        var args = null;
+        var id = null;
+
+        function _ () {
+            callback.apply(this_, args);
+            this_ = null;
+            args = null;
+            id = null;
+        }
+
+        return function () {
+            if (id !== null && this_ !== this)
+                _();
+            clearTimeout(id);
+            this_ = this;
+            args = arguments;
+            id = setTimeout(_, wait);
+        };
+    }
+
+    function pluck (prop, seq) {
+        return map(lookup(prop), seq);
+    }
+
+    Object.assign(this, {
+        allKeys: allKeys,
+        argcd: argcd,
+        argv: argv,
+        arrayify: arrayify,
+        clamp: clamp,
+        compose: compose,
+        construct: construct,
+        contains: contains,
+        counter: counter,
+        debounce: debounce,
+        each: each,
+        eachUntil: some,
+        eachr: eachr,
+        eachrUntil: eachrUntil,
+        every: every,
+        filter: filter,
+        foldl: foldl,
+        foldr: foldr,
+        getter: getter,
+        head: head,
+        I: I,
+        insertBefore: insertBefore,
+        ipairs: ipairs,
+        irange: irange,
+        isFunction: isFunction,
+        isString: isString,
+        K: K,
+        lacks: lacks,
+        last: last,
+        len: len,
+        lookup: lookup,
+        map: map,
+        mapr: mapr,
+        mapValues: mapValues,
+        max: max,
+        min: min,
+        most: most,
+        new_: new_,
+        none: none,
+        pluck: pluck,
+        range: range,
+        repeat: repeat,
+        seqEqual: seqEqual,
+        setter: setter,
+        slice: slice,
+        some: some,
+        stash: stash,
+        tail: tail,
+        transform: transform,
+        unbind: unbind,
+        volatile: volatile,
+        without: without,
+
+        VERSION: 0
+    });
+
+}).call(typeof exports !== "undefined" ? exports : (this.yf = {}));
diff --git a/test/jshint.config b/test/jshint.config
new file mode 100644 (file)
index 0000000..3a5ca20
--- /dev/null
@@ -0,0 +1,34 @@
+{
+    "browser": true,
+    "laxbreak": true,
+    "globalstrict": true,
+    "validthis": true,
+    "devel": true,
+    "unused": "vars",
+    "camelcase": true,
+    "eqeqeq": true,
+    "latedef": true,
+    "nonew": true,
+    "undef": true,
+    "trailing": true,
+    "globals": {
+        "Event": false,
+        "Promise": false,
+        "Hammer": false,
+        "Buffer": false,
+        "gl": false,
+        "yf": false,
+        "yT": false,
+        "yuu": false,
+        "ystorage": false,
+        "mat4": false,
+        "vec3": false,
+        "vec2": false,
+        "quat": false,
+        "exports": false,
+        "module": false,
+        "process": false,
+        "require": false,
+        "escape": true
+     }
+}
diff --git a/test/spec/yuu/core.js b/test/spec/yuu/core.js
new file mode 100644 (file)
index 0000000..13098e2
--- /dev/null
@@ -0,0 +1,105 @@
+var JS = this.JS || require('jstest');
+var yuu = require('yuu/core');
+
+JS.Test.describe('yuu core', function () { with (this) {
+    it("knows signum", function () { with (this) {
+        assertEqual(-1, Math.sign(-10));
+        assertEqual(1, Math.sign(10));
+        assertEqual(0, Math.sign(0));
+    }});
+    it("knows signum for weird numbers", function () { with (this) {
+        assertEqual(-1, Math.sign(-Infinity));
+        assertEqual(1, Math.sign(Infinity));
+        assert(Number.isNaN(Math.sign(NaN)));
+    }});
+
+    it("String endsWith", function () { with (this) {
+        assert("12345".endsWith("12345"));
+        assert("12345".endsWith("45"));
+        assert("12345".endsWith("5"));
+        assert("12345".endsWith(""));
+        assertNot("12345".endsWith("0"));
+        assertNot("12345".endsWith("35"));
+        assertNot("12345".endsWith("34"));
+    }});
+
+    it("String startsWith", function () { with (this) {
+        assert("12345".startsWith("12345"));
+        assert("12345".startsWith("12"));
+        assert("12345".startsWith("1"));
+        assert("12345".startsWith(""));
+        assertNot("12345".startsWith("0"));
+        assertNot("12345".startsWith("13"));
+        assertNot("12345".startsWith("23"));
+    }});
+
+    it("splits path extensions", function () { with (this) {
+        assertEqual(["a", ".png"], yuu.splitPathExtension("a.png"));
+        assertEqual(["a.b", ".png"], yuu.splitPathExtension("a.b.png"));
+        assertEqual(["a", ""], yuu.splitPathExtension("a"));
+        assertEqual(["a.b/c", ".png"], yuu.splitPathExtension("a.b/c.png"));
+        assertEqual([".gitignore", ""], yuu.splitPathExtension(".gitignore"));
+    }});
+
+    JS.Test.describe("resourcePath", function () { with (this) {
+        var f = yuu.resourcePath;
+        it("leaves ordinary paths alone", function () { with (this) {
+            assertEqual("foo.png", f("foo.png", "error", "error"));
+            assertEqual("foo/bar.png", f("foo/bar.png", "error", "error"));
+            assertEqual("foo@2x.png", f("foo@2x.png", "error", "error"));
+        }});
+        it("expands @ paths", function () { with (this) {
+            assertEqual("data/images/foo.png", f("@foo", "images", "png"));
+            assertEqual("data/images/foo.png", f("@foo.png", "images", "png"));
+            assertEqual("data/images/foo.jpg", f("@foo.jpg", "images", "png"));
+            assertEqual("data/images/foo/bar.png", f("@foo/bar", "images", "png"));
+            assertEqual("foo/data/images/bar.png", f("foo/@bar", "images", "png"));
+        }});
+        it("expands yuu/@ paths", function () { with (this) {
+            assertEqual(yuu.PATH + "data/images/bar.png",
+                        f("yuu/@bar", "images", "png"));
+        }});
+    }});
+
+    JS.Test.describe('lerping', function () { with (this) {
+        it("lerps numbers", function () { with (this) {
+            assertEqual(0.0, (0).lerp(1, 0.0));
+            assertEqual(0.5, (0).lerp(1, 0.5));
+            assertEqual(1.0, (0).lerp(1, 1.0));
+        }});
+
+        var arrayTypes = {
+            "untyped": Array,
+            "float32": Float32Array,
+            "float64": Float64Array,
+            "int8": Int8Array,
+            "uint8": Uint8Array,
+            "int16": Int16Array,
+            "uint16": Uint16Array,
+            "int32": Int32Array,
+            "uint32": Uint32Array
+        };
+
+        Object.keys(arrayTypes).forEach(function (name) {
+            var A = arrayTypes[name];
+            it("lerps " + name + " arrays element-wise", function () { with (this) {
+                var a = new A([0, 0, 0]);
+                var b = new A([0, 10, 20]);
+                assertEqual(a, a.lerp(b, 0));
+                assertEqual(b, a.lerp(b, 1));
+                assertEqual(new A([0, 5, 10]), a.lerp(b, 0.5));
+            }});
+        });
+
+        Object.keys(arrayTypes).forEach(function (name) {
+            var A = arrayTypes[name];
+            it("slices " + name + " arrays", function () { with (this) {
+                var a = new A([0, 1, 2, 3, 4, 5]);
+                var b = a.slice();
+                assertEqual(a, b);
+                assertKindOf(A, b);
+                assertNotSame(a, b);
+            }});
+        });
+    }});
+}});
diff --git a/test/spec/yuu/input.js b/test/spec/yuu/input.js
new file mode 100644 (file)
index 0000000..1bd00e4
--- /dev/null
@@ -0,0 +1,146 @@
+var JS = this.JS || require('jstest');
+var yuu = require('yuu/input');
+
+JS.Test.describe('yuu input', function () { with (this) {
+    it("has consistent key names", function () { with (this) {
+        Object.keys(yuu.KEY_NAMES).forEach(function (code) {
+            assertEqual(yuu.KEY_NAMES[code], yuu.keyCodeName(code));
+        });
+    }});
+    it("handles unknown key names", function () { with (this) {
+        Object.keys(yuu.KEY_NAMES).forEach(function (name) {
+            assertEqual("key:123456", yuu.keyCodeName(123456));
+        });
+    }});
+    it("handles event-known key names", function () { with (this) {
+        Object.keys(yuu.KEY_NAMES).forEach(function (name) {
+            assertEqual("bogus", yuu.keyCodeName(123456, "BOGUS"));
+        });
+    }});
+    it("ignores browser default names", function () { with (this) {
+        Object.keys(yuu.KEY_NAMES).forEach(function (name) {
+            assertEqual("key:123456", yuu.keyCodeName(123456, "Unidentified"));
+        });
+    }});
+
+    JS.Test.describe('KeyBindSet', function () { with (this) {
+        it("holds binds", function () { with (this) {
+            var bindset = new yuu.KeyBindSet();
+            assertEqual(0, bindset.binds.length);
+            bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' });
+            assertEqual(2, bindset.binds.length);
+        }});
+
+        it("binds new binds", function () { with (this) {
+            var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' });
+            assertEqual(2, bindset.binds.length);
+            bindset.bind('c', 'c');
+            assertEqual(3, bindset.binds.length);
+        }});
+
+        it("doesn't bind duplicates", function () { with (this) {
+            var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' });
+            assertEqual(2, bindset.binds.length);
+            bindset.bind("a", "a different a");
+            assertEqual(2, bindset.binds.length);
+            bindset.bind("a+b", "a and b");
+            assertEqual(3, bindset.binds.length);
+            bindset.bind("b+a", "b and a");
+            assertEqual(3, bindset.binds.length);
+        }});
+
+        it("unbinds", function () { with (this) {
+            var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' });
+            assertEqual(2, bindset.binds.length);
+            bindset.unbind('a');
+            assertEqual(1, bindset.binds.length);
+            bindset.unbind('x');
+            assertEqual(1, bindset.binds.length);
+            bindset.bind('a', 'a');
+            assertEqual(2, bindset.binds.length);
+        }});
+    }});
+
+    JS.Test.describe('InputState', function () { with (this) {
+        var bindset1 = new yuu.KeyBindSet({
+            "a": "a",
+            "b": "++b",
+            "c": "+c",
+            "a+b": "a and b",
+        });
+        var bindset2 = new yuu.KeyBindSet({
+            "a": "a different a",
+            "a+b+c": "a and b and c",
+            "mousemove": "mouse moved",
+            "d+mousemove": "mouse moved and d",
+        });
+
+        it("activates simple binds", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(["a"], input.keydown("a"));
+            assertNot(input.keyup("a"));
+            assertEqual(["a"], input.keydown("a"));
+            assertNot(input.keyup("a"));
+        }});
+
+        it("doesn't activate nothing", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(null, input.keydown("x"));
+            assertEqual(null, input.keyup("x"));
+        }});
+
+        it("doesn't repeat", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(["a"], input.keydown("a"));
+            assertEqual([], input.keydown("a"));
+            input.keyup("a");
+            assertEqual(["a"], input.keydown("a"));
+        }})
+
+        it("activates toggle binds", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(["++b"], input.keydown("b"));
+        }});
+
+        it("activates flag binds", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(["+c"], input.keydown("c"));
+            assertEqual([], input.keydown("c"));
+            assertEqual(["-c"], input.keyup("c"));
+        }});
+
+        it("handles multiple keys", function () { with (this) {
+            var input = new yuu.InputState([bindset1]);
+            assertEqual(["a"], input.keydown("a"));
+            assertEqual(["a and b"], input.keydown("b"));
+        }});
+
+        it("handles change states", function () { with (this) {
+            var input = new yuu.InputState([bindset2]);
+            assertEqual(["mouse moved"], input.mousemove());
+            assertEqual(["mouse moved"], input.mousemove());
+        }});
+
+        it("handles change states with a key", function () { with (this) {
+            var input = new yuu.InputState([bindset2]);
+            assertEqual(null, input.keydown("d"));
+            assertEqual(["mouse moved and d"], input.mousemove());
+            assertEqual(["mouse moved and d"], input.mousemove());
+            assertEqual(null, input.keyup("d"));
+            assertEqual(["mouse moved"], input.mousemove());
+        }});
+
+        it("masks lower sets", function () { with (this) {
+            var input = new yuu.InputState([bindset1, bindset2]);
+            assertEqual(["a different a"], input.keydown("a"));
+        }});
+
+        it("always picks the longest command", function () { with (this) {
+            var input = new yuu.InputState([bindset1, bindset2]);
+            input.keydown("b");
+            assertEqual(["a and b"], input.keydown("a"));
+            assertEqual(["a and b and c"], input.keydown("c"));
+        }});
+
+    }});
+}});
diff --git a/test/spec/yuu/storage.js b/test/spec/yuu/storage.js
new file mode 100644 (file)
index 0000000..3361d5b
--- /dev/null
@@ -0,0 +1,113 @@
+var JS = this.JS || require('jstest');
+require('yuu/pre');
+var ystorage = require('yuu/storage');
+
+JS.Test.describe('ystorage', function () { with (this) {
+    JS.Test.describe("PrefixedStorage", function () { with (this) {
+        before(function () {
+            this.backend = new ystorage.FakeStorage();
+            this.a = new ystorage.PrefixedStorage(this.backend, "a");
+            this.b = new ystorage.PrefixedStorage(this.backend, "b");
+            this.c1 = new ystorage.PrefixedStorage(this.backend, "c");
+            this.c2 = new ystorage.PrefixedStorage(this.backend, "c");
+        });
+
+        it('starts empty', function () { with (this) {
+            assertEqual(0, this.backend.length);
+            assertEqual(0, this.a.length);
+            assertEqual(0, this.b.length);
+            assertEqual(0, this.c1.length);
+            assertEqual(0, this.c2.length);
+            assertEqual(null, this.backend.key(0));
+            assertEqual(null, this.a.key(0));
+            assertEqual(null, this.b.key(0));
+            assertEqual(null, this.c1.key(0));
+            assertEqual(null, this.c2.key(0));
+        }});
+
+        it('stores and retrieves a value', function () { with (this) {
+            this.a.setItem("key", "value");
+            assertEqual(1, this.backend.length);
+            assertEqual(1, this.a.length);
+            assertEqual(0, this.b.length);
+            assertEqual(0, this.c1.length);
+            assertEqual(0, this.c2.length);
+            assertEqual("a -- key", this.backend.key(0));
+            assertEqual("key", this.a.key(0));
+            assertEqual(null, this.b.key(0));
+            assertEqual(null, this.c1.key(0));
+            assertEqual(null, this.c2.key(0));
+        }});
+
+        it('avoids other values when clearing', function () { with (this) {
+            this.a.setItem("key", "value a");
+            this.b.setItem("key", "value b");
+            assertEqual(2, this.backend.length);
+            assertEqual(1, this.a.length);
+            assertEqual(1, this.b.length);
+            this.a.clear();
+            assertEqual(1, this.backend.length);
+            assertEqual(0, this.a.length);
+            assertEqual(1, this.b.length);
+            assertEqual(null, this.a.key(0));
+            assertEqual(null, this.a.getItem("key"));
+            assertEqual("key", this.b.key(0));
+            assertEqual("value b", this.b.getItem("key"));
+        }});
+
+        it('shares with the same prefix', function () { with (this) {
+            this.c1.setItem("key", "value");
+            assertEqual("key", this.c1.key(0));
+            assertEqual("key", this.c2.key(0));
+            assertEqual("value", this.c1.getItem("key"));
+            assertEqual("value", this.c2.getItem("key"));
+        }});
+
+    }});
+
+    JS.Test.describe("PrefixedStorage", function () { with (this) {
+        it('stores objects', function () { with (this) {
+            var storage = new ystorage.Storage();
+            assertEqual(undefined, storage.getObject("key"));
+            storage.setObject("key", [1, 2, 3]);
+            assertEqual([1, 2, 3], storage.getObject("key"));
+        }});
+        it('handles defaults', function () { with (this) {
+            var storage = new ystorage.Storage(null, {
+                'should be null': null,
+                'should be undefined': undefined,
+                'should be false': false
+            });
+            assertEqual(null, storage.getObject('should be null'));
+            assertEqual(false, storage.getObject('should be false'));
+            assertEqual(undefined, storage.getObject('should be undefined'));
+            assertEqual(undefined, storage.getObject('should not exist'));
+        }});
+        it('handles fallbacks', function () { with (this) {
+            var storage = new ystorage.Storage();
+            assertEqual(undefined, storage.getObject('?', undefined));
+            assertEqual(null, storage.getObject('?', null));
+            assertEqual(false, storage.getObject('?', false));
+        }});
+        it('prefers defaults to fallbacks', function () { with (this) {
+            var storage = new ystorage.Storage(null, {
+                'should be null': null,
+                'should be undefined': undefined,
+                'should be false': false
+            });
+            assertEqual(null, storage.getObject('should be null', 0));
+            assertEqual(false, storage.getObject('should be false', 0));
+            assertEqual(undefined, storage.getObject('should be undefined', 0));
+            assertEqual(0, storage.getObject('should not exist', 0));
+        }});
+        it('keeps defaults', function () { with (this) {
+            var storage = new ystorage.Storage(null, { a: 'a' });
+            assertEqual('a', storage.getObject('a'));
+            storage.setObject('a', 'b');
+            assertEqual('b', storage.getObject('a'));
+            storage.removeObject('a');
+            assertEqual('a', storage.getObject('a'));
+        }});
+    }})
+
+}});
diff --git a/test/spec/yuu/yT.js b/test/spec/yuu/yT.js
new file mode 100644 (file)
index 0000000..dc07ccb
--- /dev/null
@@ -0,0 +1,227 @@
+var JS = this.JS || require('jstest');
+var yT = this.yT || require('yuu/yT');
+
+JS.Test.describe('yT', function() { with (this) {
+
+    it("makes Object subclasses", function () { with (this) {
+        var A = yT(Object, {});
+        assertKindOf(Object, new A());
+    }});
+    it("makes Object subclasses from a prototype", function () { with (this) {
+        var A = yT(Object.prototype, {});
+        assertKindOf(Object, new A());
+    }});
+    it("makes a non-Object subclass", function () { with (this) {
+        var A = yT(null, {});
+        var a = new A();
+        assertKindOf("object", a);
+        assertNot(a instanceof Object);
+    }});
+    it("makes Object subclasses by default", function () { with (this) {
+        var A = yT({});
+        assertKindOf(Object, new A());
+    }});
+    it("makes nested subclasses", function () { with (this) {
+        var A = yT({});
+        var B = yT(A, {});
+        assertKindOf(Object, new B());
+        assertKindOf(A, new B());
+        assertKindOf(B, new B());
+    }});
+    it("makes nested subclasses from a prototype", function () { with (this) {
+        var A = yT({});
+        var B = yT(A.prototype, {});
+        assertKindOf(Object, new B());
+        assertKindOf(A, new B());
+        assertKindOf(B, new B());
+    }});
+
+    it("sets constructors", function () { with (this) {
+        function ctor () {};
+        var A = yT({ constructor: ctor });
+        var a = new A();
+        assertSame(A, a.constructor);
+        assertSame(ctor, a.constructor);
+    }});
+    it("auto-creates root constructors", function () { with (this) {
+        var A = yT({});
+        var a = new A();
+        assertSame(A, a.constructor);
+    }});
+    it("auto-creates parent-calling constructors", function () { with (this) {
+        var A = yT({ constructor: function (x, y) { this.i = x + y; } });
+        var B = yT(A, {});
+        var b = new B(1, 2);
+        assertEqual(3, b.i);
+        assertSame(B, b.constructor);
+        assertNotSame(A, b.constructor);
+    }});
+
+    it("creates data descriptors", function () { with (this) {
+        var A = yT({ X: { value: 1 } });
+        var a = new A();
+        assertEqual(1, a.X);
+        a.X = 2;
+        assertEqual(1, a.X, "X should not be writable");
+    }});
+    it("creates writable data descriptors", function () { with (this) {
+        var A = yT({ x: { value: 1, writable: true } });
+        var a = new A();
+        assertEqual(1, a.x);
+        a.x = 2;
+        assertEqual(2, a.x);
+    }});
+    it("creates bare value descriptors", function () { with (this) {
+        var A = yT({ X: 1 });
+        var a = new A();
+        assertEqual(1, a.X);
+        a.X = 2;
+        assertEqual(1, a.X, "X should not be writable");
+    }});
+    it("creates read-only aliases", function () { with (this) {
+        var A = yT({ a: { alias: "b", readonly: true } });
+        var a = new A();
+        a.b = "hello";
+        assertEqual("hello", a.a);
+        a.b = "world";
+        assertEqual("world", a.a);
+        a.a = "goodbye";
+        assertEqual("world", a.a);
+        assertEqual("world", a.b);
+    }});
+    it("creates read-write aliases", function () { with (this) {
+        var A = yT({ a: { alias: "b", readonly: false } });
+        var a = new A();
+        a.b = "hello";
+        assertEqual("hello", a.a);
+        a.b = "world";
+        assertEqual("world", a.a);
+        a.a = "goodbye";
+        assertEqual("goodbye", a.a);
+        assertEqual("goodbye", a.b);
+    }});
+    it("creates read-write aliases by default", function () { with (this) {
+        var A = yT({ a: { alias: "b" } });
+        var a = new A();
+        a.b = "hello";
+        assertEqual("hello", a.a);
+        a.b = "world";
+        assertEqual("world", a.a);
+        a.a = "goodbye";
+        assertEqual("goodbye", a.a);
+        assertEqual("goodbye", a.b);
+    }});
+}});
+
+JS.Test.describe('an example Point type', function() { with (this) {
+    var Point = yT({
+        constructor: function (x, y) {
+            this.x = x || 0;
+            this.y = y || 0;
+        },
+
+        x: { value: 0, chainable: true },
+        y: { value: 0, chainable: true },
+        0: { alias: "x" },
+        1: { alias: "y" },
+        xy: { swizzle: "xy" },
+        yx: { swizzle: "yx" },
+
+        angle: {
+            chainable: true,
+            get: function () {
+                return Math.atan2(this.y, this.x);
+            },
+            set: function (angle) {
+                var magnitude = this.magnitude;
+                this.x = Math.cos(angle) * magnitude;
+                this.y = Math.sin(angle) * magnitude;
+            }
+        },
+
+        magnitude: {
+            chainable: true,
+            get: function () {
+                return Math.sqrt(this.x * this.x + this.y * this.y);
+            },
+            set: function (magnitude) {
+                var angle = this.angle;
+                this.x = Math.cos(angle) * magnitude;
+                this.y = Math.sin(angle) * magnitude;
+            }
+        },
+
+        length: 2
+    });
+
+    it("constructs 0, 0 by default", function () { with (this) {
+        var p = new Point();
+        assertEqual(0, p.x);
+        assertEqual(0, p.y);
+        assertEqual(2, p.length);
+    }});
+
+
+    it("has chainable setters", function () { with (this) {
+        var p = new Point().setX(1).setY(2);
+        assertEqual(1, p.x);
+        assertEqual(2, p.y);
+    }});
+
+    it("x is also 0", function () { with (this) {
+        var p = new Point();
+        assertEqual(0, p.x);
+        assertEqual(0, p[0]);
+        p.x = 1;
+        assertEqual(1, p.x);
+        assertEqual(1, p[0]);
+        p[0] = 2;
+        assertEqual(2, p.x);
+        assertEqual(2, p[0]);
+    }});
+    it("y is also 1", function () { with (this) {
+        var p = new Point();
+        assertEqual(0, p.y);
+        assertEqual(0, p[1]);
+        p.y = 1;
+        assertEqual(1, p.y);
+        assertEqual(1, p[1]);
+        p[1] = 2;
+        assertEqual(2, p.y);
+        assertEqual(2, p[1]);
+    }});
+
+    it("x and y have swizzling", function () { with (this) {
+        var p = new Point();
+        assertEqual([0, 0], p.xy);
+        assertEqual([0, 0], p.yx);
+        p.xy = [1, 0];
+        assertEqual([1, 0], p.xy);
+        assertEqual([0, 1], p.yx);
+        p.xy = p.yx;
+        assertEqual([0, 1], p.xy);
+        assertEqual([1, 0], p.yx);
+    }});
+    
+    it("can change magnitude", function () { with (this) {
+        var p = new Point(1, 0);
+        assertEqual(1, p.magnitude);
+        p.magnitude = 10;
+        assertEqual([10, 0], p.xy);
+    }});
+    it("can change angle", function () { with (this) {
+        var delta = 1e-13;
+        var p = new Point(1, 0);
+        assertEqual(0, p.angle);
+        p.angle = Math.PI / 2;
+        assertInDelta(Math.PI / 2, p.angle, delta);
+        assertInDelta(0, p.x, delta);
+        assertInDelta(1, p.y, delta);
+        p.angle = Math.PI / 4;
+        assertInDelta(Math.PI / 4, p.angle, delta);
+        assertInDelta(Math.sqrt(2) / 2, p.x, delta);
+        assertInDelta(Math.sqrt(2) / 2, p.y, delta);
+        assertInDelta(p.x, p.y, delta);
+    }});
+}});
+
diff --git a/test/spec/yuu/yf.js b/test/spec/yuu/yf.js
new file mode 100644 (file)
index 0000000..5205aad
--- /dev/null
@@ -0,0 +1,227 @@
+var JS = this.JS || require('jstest');
+require('yuu/pre');
+var yf = this.yf || require('yuu/yf');
+
+JS.Test.describe('yf', function() { with (this) {
+
+    it("allKeys", function () { with (this) {
+        assert(yf.contains(yf.allKeys(Object(1)), "hasOwnProperty"));
+        assertNot(yf.contains(yf.allKeys(Object(1)), "not a property"));
+    }});
+
+    it('argcd', function () { with (this) {
+        var f = yf.argcd(
+            function () { return 0; },
+            function (a) { return 1; },
+            function (a, b, c, d) { return 4; }
+        );
+        assertEqual(0, f());
+        assertEqual(1, f(""));
+        assertEqual(4, f("", "", "", ""));
+        assertThrows(TypeError, f.bind(null, "", ""));
+    }});
+
+    it('arrayify', function () { with (this) {
+        assertEqual([1], yf.arrayify(1));
+        assertEqual([1], yf.arrayify([1]));
+    }});
+
+    it('clamp', function () { with (this) {
+        assertEqual(0.5, yf.clamp(0.5, 0, 1));
+        assertEqual(1, yf.clamp(10, 0, 1));
+        assertEqual(0, yf.clamp(-1, 0, 1));
+    }})
+
+    it('compose', function () { with (this) {
+        function inc (a) { return a + 1; }
+        function add (a, b) { return a + b; }
+
+        var plus1 = yf.compose(inc);
+        var plus2 = yf.compose(inc, inc);
+        var plus3 = yf.compose(inc, inc, inc);
+        var addplus3 = yf.compose(plus3, add);
+
+        assertEqual(1, plus1(0));
+        assertEqual(2, plus2(0));
+        assertEqual(3, plus3(0));
+        assertEqual(6, addplus3(1, 2));
+
+        function this_ () { return this; }
+        var f = yf.compose(this_, this_, this_);
+        assertSame(this, f.call(this));
+    }});
+
+    it('construct', function () { with (this) {
+        assertEqual([], yf.construct(Array, []));
+        assertEqual([1, 2, 3], yf.construct(Array, [1, 2, 3]));
+    }});
+
+    it('counter', function () { with (this) {
+        var c0 = yf.counter(0);
+        var c5 = yf.counter(5);
+        assertEqual(0, c0());
+        assertEqual(1, c0());
+        assertEqual(2, c0());
+        assertEqual(5, c5());
+        assertEqual(6, c5());
+    }});
+
+    it('new_', function () { with (this) {
+        var a = yf.new_(Array);
+        assertEqual([], a());
+        assertEqual([1, 2, 3], a(1, 2, 3));
+    }});
+
+    it('foldl', function () { with (this) {
+        function sumString (v, i) { return v + i.toString(); }
+        var a = [1, 2, 3];
+        assertEqual("123", yf.foldl(sumString, a));
+        assertEqual("0123", yf.foldl(sumString, a, "0"));
+    }});
+
+    it('foldr', function () { with (this) {
+        function sumString (i, v) { return v + i.toString(); }
+        var a = [1, 2, 3];
+        assertEqual("321", yf.foldr(sumString, a));
+        assertEqual("4321", yf.foldr(sumString, a, "4"));
+    }});
+
+    JS.Test.describe('isFunction', function() { with (this) {
+        it("detects functions", function () { with (this) {
+            assert(yf.isFunction(Object));
+            assert(yf.isFunction(assert));
+            assert(yf.isFunction(yf.isFunction));
+        }});
+        it("detects non-functions", function () { with (this) {
+            assertNot(yf.isFunction(undefined));
+            assertNot(yf.isFunction(null));
+            assertNot(yf.isFunction(23));
+            assertNot(yf.isFunction(yf));
+        }});
+    }});
+
+    JS.Test.describe('isString', function () { with (this) {
+        it("detects strings", function () { with (this) {
+            assert(yf.isString(""));
+            assert(yf.isString("hello world"));
+        }});
+        it("detects Strings", function () { with (this) {
+            assert(yf.isString(new String("")));
+            assert(yf.isString(new String("hello world")));
+        }});
+        it("detects non-strings", function () { with (this) {
+            assertNot(yf.isString(undefined));
+            assertNot(yf.isString(null));
+            assertNot(yf.isString(23));
+            assertNot(yf.isString([1, 2, 3]));
+        }});
+    }});
+
+    JS.Test.describe('insertBefore', function () { with (this) {
+        it("inserts at start", function () { with (this) {
+            var a = [1, 2];
+            yf.insertBefore(a, 0, 1);
+            assertEqual([0, 1, 2], a);
+        }});
+        it("inserts at middle", function () { with (this) {
+            var a = [0, 2];
+            yf.insertBefore(a, 1, 2);
+            assertEqual([0, 1, 2], a);
+        }});
+        it("inserts at end if missing", function () { with (this) {
+            var a = [0, 1];
+            yf.insertBefore(a, 2, 99);
+            assertEqual([0, 1, 2], a);
+        }});
+    }});
+
+    JS.Test.describe('contains', function () { with (this) {
+        var a = [1, 2, 3];
+        it("finds things", function () { with (this) {
+            assert(yf.contains(a, 1));
+            assert(yf.contains(a, 3));
+        }});
+        it("doesn't find things", function () { with (this) {
+            assertNot(yf.contains(a, -1));
+            assertNot(yf.contains(a, 4));
+        }});
+        it("handles the empty sequence", function () { with (this) {
+            assertNot(yf.contains([], 1));
+            assertNot(yf.contains([], undefined));
+            assertNot(yf.contains([], null));
+        }});
+        it("uses identity, not equality", function () { with (this) {
+            assertNot(yf.contains(a, "1"));
+        }});
+    }});
+
+    JS.Test.describe('repeat', function () { with (this) {
+        it("repeats nothing", function () { with (this) {
+            assertEqual([], yf.repeat("hello", 0));
+            assertEqual([], yf.repeat(1, 0));
+            assertEqual([], yf.repeat(null, 0));
+        }});
+        it("repeats something", function () { with (this) {
+            assertEqual([1], yf.repeat(1, 1));
+            assertEqual([1, 1], yf.repeat(1, 2));
+            assertEqual([1, 1, 1, 1], yf.repeat(1, 4));
+        }});
+    }})
+
+    JS.Test.describe('without', function () { with (this) {
+        var a = [1, 2, 3];
+        it("doesn't remove wrongly", function () { with (this) {
+            assertEqual([1, 2, 3], yf.without(a, 4));
+        }});
+        it("removes by ===", function () { with (this) {
+            assertEqual([1, 2, 3], yf.without(a, "1"));
+        }});
+        it("removes one element", function () { with (this) {
+            assertEqual([2, 3], yf.without(a, 1));
+        }});
+        it("removes multiple elements", function () { with (this) {
+            assertEqual([3], yf.without(a, 1, 2));
+            assertEqual([2], yf.without(a, 1, 3));
+            assertEqual([], yf.without(a, 1, 2, 3));
+        }});
+        it("handles the empty sequence", function () { with (this) {
+            assertEqual([], yf.without([]));
+            assertEqual([], yf.without([], 1));
+            assertEqual([], yf.without([], 1, 2));
+        }});
+    }});
+
+    it("some", function () { with (this) {
+        assert(yf.some(null, [1]));
+        assert(yf.some(null, [1, 2]));
+        assert(yf.some(null, [1, 0]));
+
+        assertNot(yf.some(null, []));
+        assertNot(yf.some(null, [0, NaN]));
+        assertNot(yf.some(null, [0]));
+    }});
+
+    it('every', function () { with (this) {
+        assert(yf.every(null, []));
+        assert(yf.every(null, [1]));
+        assert(yf.every(null, [1, 2]));
+
+        assertNot(yf.every(null, [0]));
+        assertNot(yf.every(null, [1, 0]));
+        assertNot(yf.every(null, [1, 2, NaN]));
+    }});
+
+    it('none', function () { with (this) {
+        assert(yf.none(null, []));
+        assert(yf.none(null, [0]));
+        assert(yf.none(null, [0, NaN]));
+
+        assertNot(yf.none(null, [1]));
+        assertNot(yf.none(null, [1, 0]));
+    }});
+
+    it('pluck', function () { with (this) {
+        assertEqual([], yf.pluck("foo", []));
+        assertEqual([1, 2, 3], yf.pluck("length", ["a", "ab", "abc"]));
+    }});
+}});
diff --git a/tools/LICENSE.rcedit b/tools/LICENSE.rcedit
new file mode 100644 (file)
index 0000000..437dd58
--- /dev/null
@@ -0,0 +1,57 @@
+Copyright (c) 2013 GitHub Inc.
+https://github.com/atom/rcedit/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Also it's mostly using code from Rescle that GitHub has stripped the
+copyright notice from, because who needs to follow licenses when
+you've got piles of VC money and a terrible culture literally woven
+into your furniture?
+
+--
+
+Copyright (c) 2009 Yoshio Okumura
+https://code.google.com/p/rescle/
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tools/composer/composer.js b/tools/composer/composer.js
new file mode 100644 (file)
index 0000000..75bb81c
--- /dev/null
@@ -0,0 +1,85 @@
+"use strict";
+
+function titleCase (text) {
+    return text.replace("_", " ").replace(/\w\S*/g, function (word) {
+        return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
+    });
+}
+
+function setOptions (select, dict) {
+    yf.irange.call(select, select.remove, select.options.length - 1, -1, -1);
+    yf.each(function (name) {
+        var opt = document.createElement("option");
+        opt.text = titleCase(name);
+        opt.id = name;
+        select.appendChild(opt);
+    }, Object.keys(dict).sort());
+}
+
+window.addEventListener("load", function () {
+    var playing;
+
+    yuu.init({}).then(function () {
+        setOptions(document.getElementById("instrument"), yuu.Instruments);
+        setOptions(document.getElementById("scale"), yuu.Scales);
+    });
+
+    function play () {
+        if (playing)
+            playing.disconnect();
+        var instruments = document.getElementById("instrument");
+        var scales = document.getElementById("scale");
+        var instrument = yuu.Instruments[
+            instruments.options[instruments.selectedIndex].id];
+        var scale = yuu.Scales[
+            scales.options[scales.selectedIndex].id];
+        var score = yuu.parseScore(
+            document.getElementById("score").value,
+            scale, document.getElementById("tonic").value);
+        var bps = +document.getElementById("tempo").value / 60;
+        playing = yuu.audio.createGain();
+        playing.gain.value = 0.5;
+        var t = yuu.audio.currentTime + 0.25;
+        yf.each(function (note) {
+            instrument.createSound(
+                yuu.audio,
+                t + note.time / bps,
+                note.hz,
+                1.0,
+                note.duration / bps
+            ).connect(playing);
+        }, score);
+        playing.connect(yuu.audio.destination);
+    }
+
+    function jam (key) {
+        if (key === "`")
+            key = "0";
+        else if (key === "0")
+            key = "10";
+        var instruments = document.getElementById("instrument");
+        var scales = document.getElementById("scale");
+        var tonic = document.getElementById("tonic").value;
+        var instrument = yuu.Instruments[
+            instruments.options[instruments.selectedIndex].id];
+        var scale = yuu.Scales[scales.options[scales.selectedIndex].id];
+        var note = yuu.parseScore(key, scale, tonic)[0];
+        var bps = +document.getElementById("tempo").value / 60;
+        if (note)
+            instrument.createSound(
+                yuu.audio,
+                yuu.audio.currentTime + 0.01,
+                note.hz,
+                1.0,
+                note.duration / bps
+            ).connect(yuu.audio.destination);
+    }
+
+    document.getElementById("play").addEventListener("click", play);
+
+    document.getElementById("jam")
+        .addEventListener("keydown", function (event) {
+            jam(yuu.keyEventName(event));
+        });
+});
+
diff --git a/tools/composer/index.html b/tools/composer/index.html
new file mode 100644 (file)
index 0000000..3202fab
--- /dev/null
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>Yuu Composer</title>
+    <meta charset="utf-8" />
+    <script type="text/javascript" src="../../src/yuu/pre.js"></script>
+    <script type="text/javascript" src="../../src/yuu/yf.js"></script>
+    <script type="text/javascript" src="../../src/yuu/yT.js"></script>
+    <script type="text/javascript" src="../../src/yuu/core.js"></script>
+    <script type="text/javascript" src="../../src/yuu/input.js"></script>
+    <script type="text/javascript" src="../../src/yuu/audio.js"></script>
+    <script type="text/javascript" src="composer.js"></script>
+  </head>
+  
+  <body style="margin: 5% auto; max-width: 800px">
+    <div id="jam"
+         style="float: right; width: 30%; text-align: center; line-height: 5em"
+         tabindex=1>
+      click and type ` to 0 to jam
+    </div>
+    <div style="width: 65%">
+      <textarea id="score"
+                style="min-height: 4em; width: 100%"
+                >0 1 2 3 4 5 6 7 6 5 4 3 2 1 0</textarea>
+      <br>
+      <button id="play" style="width: 100%">Play</button>
+    </div>
+    <div style="margin-top: 2em">
+      <span style="display: inline-block; width: 24%">Instrument:<br><select id="instrument"></select></span>
+      <span style="display: inline-block; width: 24%">Tonic:<br><input id="tonic" value="C4"></span>
+      <span style="display: inline-block; width: 24%">Scale:<br><select id="scale"></select></span>
+      <span style="display: inline-block; width: 24%">Tempo (BPM):<br><input id="tempo" value="120"></span>
+    </div>
+  </body>
+</html>
diff --git a/tools/generate-appcache b/tools/generate-appcache
new file mode 100755 (executable)
index 0000000..a729f0b
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+
+# Turn a website into an HTML5 application with an application cache
+# manifest.
+#
+# http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html
+# https://developer.mozilla.org/en/docs/HTML/Using_the_application_cache
+
+import os
+import re
+import shutil
+import time
+
+def is_html(filename):
+    return filename.lower().endswith(".html")
+
+def main(appdir):
+    if not os.path.isdir(appdir):
+        raise StandardError("input (%r) is not a directory" % appdir)
+    all_files = []
+    for root, dirnames, filenames in os.walk(appdir):
+        root = os.path.relpath(root, appdir)
+        for filename in filenames:
+            all_files.append(os.path.join(root, filename))
+    all_files.sort()
+    appcache = os.path.join(appdir, "manifest.appcache")
+    with open(appcache, "w") as fobj:
+        fobj.write("CACHE MANIFEST\n")
+        fobj.write("# Generated on %s\n" % time.strftime("%Y %T %z"))
+        for filename in all_files:
+            fobj.write(filename + "\n")
+
+    for filename in filter(is_html, all_files):
+        filename = os.path.join(appdir, filename)
+        # This call to relpath is the entire reason this tool is in
+        # Python and not a shell script.
+        relpath = os.path.relpath(appcache, os.path.dirname(filename))
+        html = open(filename).read()
+        html = re.sub("<html([> ])", '<html manifest="%s"\\1' % relpath, html)
+        with open(filename, "w") as fobj:
+            fobj.write(html)
+
+if __name__ == "__main__":
+    import sys
+    try:
+        appdir = sys.argv[1]
+    except IndexError:
+        raise SystemExit("Usage: %s appdir" %(
+            sys.argv[0]))
+    else:
+        main(appdir)
+        
diff --git a/tools/generate-nw b/tools/generate-nw
new file mode 100755 (executable)
index 0000000..a3cd564
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+# Generate a node-webkit package.json file for a website.
+#
+# https://github.com/rogerwang/node-webkit/wiki/Manifest-format
+
+import os
+import re
+import shutil
+import time
+import json
+
+def is_html(filename):
+    return filename.lower().endswith(".html")
+
+def attr(name):
+    return "data-" + name + """=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?"""
+
+def main(appdir):
+    if not os.path.isdir(appdir):
+        raise StandardError("input (%r) is not a directory" % appdir)
+    indexes = []
+    icons = []
+    for root, dirnames, filenames in os.walk(appdir):
+        root = os.path.relpath(root, appdir)
+        for filename in filenames:
+            if filename.lower() == "index.html":
+                indexes.append(os.path.join(root, filename))
+            if ("icon" in filename.lower()
+                and filename.lower().endswith((".ico", ".png"))):
+                icons.append(os.path.join(root, filename))
+
+    indexes.sort(key=lambda fn: (fn.count("/"), fn))
+    icons.sort(key=lambda fn: (fn.count("/"), fn))
+    name = os.path.basename(appdir).split(".")[0]
+    package = {
+        "main": indexes[0],
+        "name": name,
+        "version": "0.0.0",
+        "window": {
+            "show": False,
+            "toolbar": False,
+            "title": name,
+            "min_width": 300,
+            "min_height": 200,
+        }
+    }
+
+    if icons:
+        package["window"]["icon"] = icons[0]
+
+    with open(os.path.join(appdir, indexes[0]), "r") as fobj:
+        header = fobj.read(4096)
+        try:
+            title = re.search("<title>([^<]+)<", header).groups()[0]
+        except AttributeError:
+            print >>sys.stderr, "No <title> found in %r." % fobj.name
+        else:
+            package["window"]["title"] = title
+        try:
+            version = re.search(attr("version"), header).groups()[0]
+        except AttributeError:
+            print >>sys.stderr, "No version found in %r." % fobj.name
+        else:
+            package["version"] = version
+        try:
+            appid = re.search(attr("appid"), header).groups()[0]
+        except AttributeError:
+            print >>sys.stderr, "No appid found in %r." % fobj.name
+        else:
+            package["name"] = appid
+
+    if "+" not in package["version"]:
+        package["version"] += "+"
+    package["version"] += time.strftime("%Y%m%d%H%M%S")
+
+    with open(os.path.join(appdir, "package.json"), "w") as fobj:
+        json.dump(package, fobj)
+
+if __name__ == "__main__":
+    import sys
+    try:
+        appdir = sys.argv[1]
+    except IndexError:
+        raise SystemExit("Usage: %s appdir" %(
+            sys.argv[0]))
+    else:
+        main(appdir)
+        
diff --git a/tools/generate-osx-app b/tools/generate-osx-app
new file mode 100755 (executable)
index 0000000..dd05cdd
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+
+import os
+import zipfile
+import re
+import plistlib
+import shutil
+import json
+import re
+
+from os.path import join, basename
+
+def xp_filename(basename):
+    return re.sub('["<>*?|\\\\]', "_",
+                  basename.replace("/", "-").replace(":", "."))
+
+def versionify(version):
+    return ".".join(filter(lambda x: x.isdigit(),
+                           re.split("[-+.]", version))[:3])
+
+def main(nwdir, nwpackage):
+    if not os.path.isdir(nwdir):
+        raise StandardError("input (%r) is not a directory" % nwdir)
+    nwzip = zipfile.ZipFile(nwpackage)
+    icnss = filter(lambda f: f.lower().endswith(".icns"),
+                  nwzip.namelist())
+    package = json.load(nwzip.open("package.json"))
+    app = join(nwdir, "node-webkit.app")
+    title = package["window"]["title"]
+    exe = package["name"].split(".")[-1]
+    plist = dict(
+        CFBundleDisplayName=title,
+        CFBundleExecutable=exe,
+        CFBundleIdentifier=package["name"],
+        CFBundleInfoDictionaryVersion="6.0",
+        CFBundleName=title,
+        CFBundlePackageType="APPL",
+        CFBundleShortVersionString=package["version"],
+        CFBundleIconFile="nw.icns",
+        CFBundleVersion=versionify(package["version"]),
+        LSMinimumSystemVersion="10.6.0",
+        NSPrincipalClass="NSApplication",
+        NSSupportsAutomaticGraphicsSwitching=True,
+    )
+    if icnss:
+        icnss.sort()
+        os.remove(join(app, "Contents", "Resources", "nw.icns"))
+        nwzip.extract(icnss[0], join(app, "Contents", "Resources"))
+        plist["CFBundleIconFile"] = icnss[0]
+    plistlib.writePlist(plist, join(app, "Contents/Info.plist"))
+    exedir = join(app, "Contents", "MacOS")
+    shutil.move(join(exedir, "node-webkit"), join(exedir, exe))
+    shutil.copy(nwpackage, join(app, "Contents", "Resources", "app.nw"))
+    shutil.move(app, join(app, "..", xp_filename(title) + ".app"))
+
+if __name__ == "__main__":
+    import sys
+    main(*sys.argv[1:])
+
diff --git a/tools/nw-linux-wrapper b/tools/nw-linux-wrapper
new file mode 100755 (executable)
index 0000000..d13e39f
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# https://github.com/rogerwang/node-webkit/issues/136
+# https://github.com/rogerwang/node-webkit/wiki/The-solution-of-lacking-libudev.so.0
+#
+# Though that page would be more accurately titled "apparently even
+# Intel, Google, and every commercial distribution can't be bothered
+# to maintain trivial compatibility on desktop Linux."
+
+SELF=$(readlink -f "$0")
+NWDIR=$(dirname "$SELF")/nw
+NWBIN=$(command -v "$NWDIR/nw" nw | head -n 1)
+PACKAGE="$NWDIR/package.nw"
+
+if [ ! -x "$NWBIN" ]; then
+    echo "node-webkit (nw) executable could not be found." >&2
+    exit 127
+fi
+
+"$NWBIN" "$@" "$PACKAGE"
+if [ "$?" = "127" -a -w "$NWBIN" ]; then
+    echo "Applying libudev.so.1 hack." >&2
+    sed -i 's/udev\.so\.0/udev.so.1/g' "$NWBIN"
+    "$NWBIN" "$@" "$PACKAGE"
+fi
diff --git a/tools/rcedit.exe b/tools/rcedit.exe
new file mode 100644 (file)
index 0000000..9a8cb71
Binary files /dev/null and b/tools/rcedit.exe differ