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");
+     &nbs