From: Joe Wreschnig Date: Mon, 22 Sep 2014 00:00:36 +0000 (+0200) Subject: Initial import. X-Git-Url: https://git.yukkurigames.com/?p=yuu.git;a=commitdiff_plain;h=6be6217d1fda27ec6c168466111cd26e52bf59a2 Initial import. --- 6be6217d1fda27ec6c168466111cd26e52bf59a2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5966153 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +.gitattributes export-ignore +.gitignore export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17918bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*~ +\#* +.DS_Store +node_modules +build +node-webkit + diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..7865c8b --- /dev/null +++ b/.projectile @@ -0,0 +1,2 @@ +-/node_modules +-/node-webkit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab7e9e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,101 @@ +#!/usr/bin/make -f + +node-webkit-version := 0.10.4 + +.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 := yuu +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 `echo $(*F) | sed -E 's/.+_[^0-9]+//'` + cd $@.tmp && $(ZIP) -r ../$(@F) . + $(RM) -r $@.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..a265ea9 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Yuu Game Library + +This is the library used for several of our games at +[Yukkuri Games](https://yukkurigames.com). + +It is released under the terms of the GNU GPL version 2 or later. diff --git a/rules/git.mk b/rules/git.mk new file mode 100644 index 0000000..e8ceb78 --- /dev/null +++ b/rules/git.mk @@ -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 index 0000000..eba5e89 --- /dev/null +++ b/rules/icons.mk @@ -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 index 0000000..15fb8a5 --- /dev/null +++ b/rules/javascript.mk @@ -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 index 0000000..dd46e17 --- /dev/null +++ b/rules/node-webkit.mk @@ -0,0 +1,99 @@ +# 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-directx = $(addprefix $(node-webkit-prefix),d3dcompiler_43.dll d3dcompiler_46.dll) + +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 + cp -a $(node-webkit-directx) $(@:.zip=) + 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) $(node-webkit-directx) + $(call node-webkit-package-win,$<,$(word 2,$^)) + +$(node-webkit-directx): + wget -O $@ 'https://github.com/cefsharp/cef-binary/raw/1e51255cf77d267899bf7834768b8774affaad2d/cef_binary_3.y.z_windows32/Release/'$(notdir $@) + +$(node-webkit-prefix)dxwebsetup.exe: + wget -O $@ http://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe diff --git a/rules/pngcrush.mk b/rules/pngcrush.mk new file mode 100644 index 0000000..39630df --- /dev/null +++ b/rules/pngcrush.mk @@ -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 index 0000000..d706d3f --- /dev/null +++ b/rules/programs.mk @@ -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/face.png b/src/data/images/face.png new file mode 100644 index 0000000..1918897 Binary files /dev/null and b/src/data/images/face.png differ diff --git a/src/data/images/logotype_horizontal_1.png b/src/data/images/logotype_horizontal_1.png new file mode 100644 index 0000000..a845d24 Binary files /dev/null and b/src/data/images/logotype_horizontal_1.png differ diff --git a/src/data/images/logotype_horizontal_2.png b/src/data/images/logotype_horizontal_2.png new file mode 100644 index 0000000..7ae1d09 Binary files /dev/null and b/src/data/images/logotype_horizontal_2.png differ diff --git a/src/data/images/text.png b/src/data/images/text.png new file mode 100644 index 0000000..4982347 Binary files /dev/null and b/src/data/images/text.png differ diff --git a/src/data/license.txt b/src/data/license.txt new file mode 100644 index 0000000..fdd9313 --- /dev/null +++ b/src/data/license.txt @@ -0,0 +1,15 @@ +This demonstration project, aside from the Yukkuri Games logo, is +released into the public domain. + +The person who associated a work with this deed has dedicated the work +to the public domain by waiving all of his or her rights to the work +worldwide under copyright law, including all related and neighboring +rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for +commercial purposes, all without asking permission. + +See https://creativecommons.org/publicdomain/zero/1.0/ for details. + +The source code under src/yuu remains under the terms of the GNU GPL +versions 2 or later, unless specified otherwise in the files. diff --git a/src/ext/FiraMono-Bold.woff b/src/ext/FiraMono-Bold.woff new file mode 100644 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 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 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 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 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 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 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 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 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 index 0000000..62e8856 --- /dev/null +++ b/src/ext/gamepad.js @@ -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 + + + + + + + + + + + + + + +
+ + + Yukkuri Games + +

Yuu

+
+
+ + + Yuu requires JavaScript, canvas, and WebGL support. Your + browser seems to lack canvas support. + +
+
+

There was a problem. Sorry about that.

+

+

Error Log

+
+        
+
+
+

+ There was a serious problem. You'll have to restart. Sorry + about that. +

+

+

+ If this problem continues, + check our + support page. If it doesn't help, + email us. + Please include the rest of the text in this error message and + information about your + system's GPU. +

+

+ Supported browsers include recent versions of + Mozilla Firefox + and Google Chrome + on most desktop computers, + and Chrome + for Android +

+

+ If you are running one of these browsers and still have a problem, + check our + support page. If it doesn't help, + email us. + Please include the rest of the text in this error message and + as much information as you can about your operating system, + computer, and especially GPU. +

+

Error Log

+
+        
+
+

+ Yuu is our JavaScript/WebGL game + engine. It is currently being used in + Pixel Witch Lesson #6 + and other games in development, as well as the live demo + below. (In other words, if you can see the demo you can play + our games — and if there's an error, please + let us know.) +

+

+ Games made with Yuu run in Chromium and Firefox, and can also + be automatically packaged with + node-webkit + for Windows, Mac OS X, and GNU/Linux. It exclusively targets + "modern" JavaScript implementations because there's already a + hard requirement on WebGL. +

+

+ Because it's based on WebGL it theoretically supports + any platform implementing HTML5, ES5, and WebGL. In practice, + many such implementations are buggy and idiosyncratic. Android + 4.4 support is good on several phone lines; iOS 8 (and Safari + on OS X) support is not. +

+

Getting It & Using It

+

+ Yuu provides no stability guarantees. It's an + immature library and likely to see major breaking changes + regularly. It is a relatively low-level library, and there is + little documentation. (Hopefully, these issues are going to + shake out as we make more games.) +

+

+ Because of this, it's currently only distributed with games + using it, or from our Git repository: +

+ +

+ Yuu 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. +

+

+ Yuu also contains code from other projects, all of which is + licensed under GPL-compatible terms. If you're having trouble + sleeping, it includes + complete license text. +

+
+ + diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..fa4d395 --- /dev/null +++ b/src/main.css @@ -0,0 +1,527 @@ +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 300; + src: local('Fira Sans OT Light'), local('Fira Sans Light'), url(FiraSans-Light.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: italic; + font-weight: 300; + src: local('Fira Sans OT Light Italic'), local('Fira Sans Light Italic'), url(FiraSans-LightItalic.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 400; + src: local('Fira Sans OT'), local('Fira Sans'), url(FiraSans-Regular.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 700; + src: local('Fira Sans OT Bold'), local('Fira Sans Bold'), url(FiraSans-Bold.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: italic; + font-weight: 400; + src: local('Fira Sans OT Italic'), local('Fira Sans Italic'), url(FiraSans-Italic.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: italic; + font-weight: 700; + src: local('Fira Sans OT Bold Italic'), local('Fira Sans Bold Italic'), url(FiraSans-BoldItalic.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Mono'; + font-style: normal; + font-weight: 400; + src: local('Fira Mono OT'), local('Fira Mono'), url(FiraMono-Regular.woff) format('woff'); +} + +@font-face { + font-family: 'Fira Mono'; + font-style: normal; + font-weight: 700; + src: local('Fira Mono OT Bold'), local('Fira Mono Bold'), url(FiraMono-Bold.woff) format('woff'); +} + +* { + padding: 0; + margin: 0; + font-weight: normal; + text-decoration: none; +} + +header * { + outline: none; +} + +strong { font-weight: bold; } +em { font-style: italic; } + +a:link, [onclick] { + text-decoration: underline; + cursor: pointer; +} + +html { + font-family: "Fira Sans", sans-serif; + font-size: 16px; + background-color: rgb(226, 192, 242); + background-image: linear-gradient(rgb(226, 192, 242), rgb(244, 199, 199) 100%); + background-attachment: fixed; + padding: 0 1em; + height: 100%; + background-repeat: no-repeat; +} + +body { + border-top: solid rgb(206, 132, 242) 0.75em; + border-bottom: solid rgb(244, 126, 126) 0.75em; + border-radius: 2em; + margin: 0.25em auto; + color: black; + background-color: white; + max-width: 56em; + padding: 0 1em; +} + +header { + border-top: solid rgb(206, 132, 242) 0.5em; + border-bottom: solid rgb(244, 126, 126) 0.5em; + border-radius: 2em; + height: 6em; + transition: border-color 0.5s; + white-space: nowrap; + text-align: right; + margin: 0 -1em; + margin-top: -0.75em; + background-color: white; +} + +header img { + float: left; + height: 100%; + width: auto; +} + +header h1 { + font-size: 3em; + font-weight: normal; + margin-right: 0.25em; + display: inline-block; + height: 100%; + line-height: 2em; +} + +header:hover { + border-top-color: rgb(244, 126, 126); + border-bottom-color: rgb(206, 132, 242); +} + +img.logo { + -webkit-transition: -webkit-transform 1.5s; + transition: transform 1.5s; + -webkit-transition-delay: 0.5s; + transition-delay: 0.5s; + -webkit-transition-timing-function: cubic-bezier(0.4, 0.2, 0.5, 1.3); + transition-timing-function: cubic-bezier(0.4, 0.2, 0.5, 1.3); +} + +img.logo:hover { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); +} + +h2 { + border-top: solid rgb(206, 132, 242) 1px; + border-left: solid rgb(206, 132, 242) 1px; + border-radius: 1em 0 0 1em; + font-size: 1.25em; + font-weight: 300; + margin-left: -0.67em; + margin-right: -0.67em; + margin-top: 1.5em; + margin-bottom: 0.5em; + padding-left: 0.5em; + padding-top: 0.125em; +} + +h3 { + border-top: solid rgb(206, 132, 242) 1px; + border-left: solid rgb(206, 132, 242) 1px; + border-radius: 1.5em; + display: inline-block; + font-size: 1.125em; + font-weight: 300; + margin-bottom: 0; + margin-left: -0.445em; + padding-left: 0.67em; + padding-right: 0.67em; +} + +a:link, [onclick] { + color: rgb(206, 132, 242); + transition: color 0.3s; +} + +a:visited { + color: rgb(206, 132, 242); +} + +a:hover, [onclick]:hover { + color: rgb(244, 126, 126); +} + +main { + display: block; + padding: 1em; + max-width: 50em; + margin: auto; +} + +main > *:first-child { + margin-top: 0; +} + +p, li { + line-height: 125%; +} + +main > p { + max-width: 45em; + margin: 1em auto; +} + +main > ul { + max-width: 40em; + margin: 1em auto; +} + +main > ul > li { + margin: 0.5em auto; +} + +hr { + margin-top: 0; + margin-bottom: 1em; + border-bottom: solid rgb(206, 132, 242) 1px; + height: 2em; +} + +.highlight { + border-bottom: solid rgb(206, 132, 242) 0.125em; + border-top: solid rgb(206, 132, 242) 0.125em; + border-radius: 1em; + font-weight: 300; + padding: 1em 3em; + transition: border-radius 0.3s; + box-sizing: border-box; +} + +.highlight:hover { + border-radius: 3em; +} + +.game-preview { + padding: 0; + text-align: center; + width: 100%; + max-width: 37em; + margin: auto; + list-style-type: none; +} + +.game-preview > li { + position: relative; + border: solid rgb(206, 132, 242) 1px; + margin-bottom: 1.5em; + margin-top: 1em; + margin-left: 2.5em; + border-left-width: 0; + height: 8em; + transition: border-radius 0.3s, border-color 0.3s; + padding-left: 4em; + box-sizing: border-box; + border-radius: 0 3em 3em 0; +} + +.game-preview > li:nth-child(even) { + padding-left: 0; + padding-right: 4em; + margin-right: 2.5em; + margin-left: 0; + border-left-width: 1px; + border-right-width: 0; + border-radius: 3em 0 0 3em; +} + +.game-preview > li:hover { + border-radius: 0 0.25em 0.25em 0; + border-color: rgb(244, 126, 126); +} + +.game-preview > li img { + border: solid rgb(206, 132, 242) 1px; + position: absolute; + border-radius: 50%; + top: -1px; + left: -4em; + height: 8em; + width: 8em; + opacity: 0.75; + transition: border-radius 0.3s, border-color 0.3s, opacity 0.3s; + box-sizing: border-box; +} + +.game-preview > li:nth-child(even) img { + left: auto; + right: -4em; +} + +.game-preview > li:hover img { + border-radius: 0; + border-color: rgb(244, 126, 126); + opacity: 1.0; +} + +.game-preview .info { + position: relative; + height: 100%; +} + +.game-preview .info h4 { + color: inherit; + font-style: italic; + padding-left: 0.125em; + padding-right: 0.125em; + text-decoration: none; + text-align: center; + box-sizing: border-box; + padding-top: 0.25em; + font-size: 1.125em; +} + +.game-preview li:nth-child(odd) .info h4, +.game-preview li:nth-child(odd) .info ul { + margin-right: 6.5rem; +} + +.game-preview li:nth-child(even) .info h4, +.game-preview li:nth-child(even) .info ul { + margin-right: 0em; + margin-left: 6.5rem; +} + +h4 a:link { + text-decoration: none; +} + +.game-preview .info p { + padding: 0.25em; + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + -webkit-transform: translateY(-50%); + margin-top: 0.125em; +} + +.game-preview .info ul { + bottom: 0.25em; + font-size: 0.75em; + position: absolute; + right: 0.125em; + left: 0.125em; + text-align: center; + list-style-type: none; +} + +.game-preview .info li { + white-space: nowrap; +} + +.game-preview .info li a:link { + text-decoration: none; + font-weight: inherit; +} + +.game-preview .info li:before { + content: ' ~ '; +} + +.game-preview .info li:first-child:before { + content: ''; +} + +.game-preview .info li { + display: inline; + font-weight: 300; +} + +ul.download { + border-radius: 1em; + border: solid rgb(206, 132, 242) 0.125em; + font-weight: 300; + padding: 0.5em; + margin: 1em auto; + transition: border-radius 0.3s, border-color 0.3s; + white-space: nowrap; + display: table; +} + +ul.download:hover { + border-radius: 4px; + border-color: rgb(244, 126, 126); +} + +ul.download li { + list-style-type: none; + font-size: 1.25em; + margin-bottom: 0.8em; + text-align: center; +} + +ul.download li a { + text-decoration: none; +} + +ul.download li.sh { + margin-bottom: 0; + font-size: 0.75em; + font-family: "Fira Mono", monospace; + text-align: left; +} + +ul.download li:last-child { + margin-bottom: 0; +} + +.sh:before { + content: "$ "; +} + +input { + font-family: inherit; + font-size: 1em; +} + +.copyright { + margin: auto; + text-align: justify; + width: 75%; + font-size: 0.875em; + font-weight: 400; +} + +.copyright > p { + font-size: 0.875em; + font-weight: 300; +} + +.copyright:before { + content: "Copyright ©"; +} + +#yuu-canvas { + margin: 1em auto; + width: 100%; + height: 20em; + position: static; + border: solid rgba(206, 132, 242, 0.5) 2px; + border-radius: 2em; +} + +pre { + border-left: solid rgba(206, 132, 242, 0.5) 8px; + border-radius: 8px; + font-family: "Fira Mono", monospace; + overflow: auto; + padding-left: 2em; + padding: 0.5em; + max-width: 40em; + margin: auto; + transition: background-color 0.3s, border-color 0.3s, border-radius 0.3s; +} + +pre:hover { + background-color: rgba(206, 132, 242, 0.125); + border-color: rgb(206, 132, 242); + border-radius: 16px; +} + +code { + font-family: "Fira Mono", monospace; + transition: background-color 0.3s; +} + +code:hover { + background-color: rgba(206, 132, 242, 0.125); +} + +pre code:hover { + background-color: transparent; +} + +footer { + font-weight: 300; + text-align: center; + font-size: 0.75em; +} + +footer a:link { + display: inline-block; + margin: 0 1em; + text-decoration: none; +} + +@media (max-width: 767px) { + html { font-size: 13px; } + + .optional { + display: none; + } + + ul.download li.sh:before { + content: ""; + } + + pre { + font-size: 0.875em; + } +} + +@media (max-width: 479px) { + html { font-size: 10px; padding: 0 0.5em; } + h4 { font-size: 1em; } +} + +body { + background-color: white; + overflow: auto; +} + +.yuu-toast { + background-color: transparent; + border: solid rgba(206, 132, 242, 0.5) 2px; + color: inherit; +} + +.yuu-overlay { + background-color: transparent; + border: solid rgb(244, 126, 126) 2px; + color: inherit; + position: static; + transform: none; +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..6b2cb7b --- /dev/null +++ b/src/main.js @@ -0,0 +1,83 @@ +"use strict"; + +var DISAPPEAR = { + 0: { tween: { transform: { yaw: Math.PI } }, + duration: 35 }, + 10: { tween: { quad: { alpha: 0 } }, + duration: 25 } +}; + +var JUMP = { + 0: { tween1: { scale: [0.6, 0.3, 1.0] }, duration: 20 }, + 20: [{ tween1: { scale: [0.3, 0.6, 1.0] }, duration: 18 }, + { tween1: { xy: 'target' }, duration: 20 }], + 38: { tween1: { scale: [0.5, 0.5, 1.0] }, duration: 5 }, +}; + +var FollowScene = yT(yuu.Scene, { + constructor: function () { + yuu.Scene.call(this); + var textMat = new yuu.Material("@text"); + var faceMat = new yuu.Material("@face"); + + var textQuad; + var textEntity = new yuu.E( + new yuu.Transform().setScale([1, 1, 1]), + textQuad = new yuu.QuadC(textMat)); + this.faceEntity = new yuu.E( + new yuu.Transform().setScale([0.5, 0.5, 1]), + new yuu.QuadC(faceMat)); + this.entity0.addChild(textEntity); + this.entity0.addChild(this.faceEntity); + + this.disappear = function () { + textEntity.attach(new yuu.Animation(DISAPPEAR, { + transform: textEntity.transform, + quad: textQuad + })); + }; + + this.ready = yuu.ready([textMat, faceMat], this); + }, + + inputs: { + resize: function () { + var base = new yuu.AABB(-1.5, -1.5, 1.5, 1.5); + 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); + var d = 0.75 * (p.x - this.faceEntity.transform.x); + var angle = Math.PI / 2 * d; + if (!angle) + angle = 0; + angle = yf.clamp(angle, -Math.PI / 2, Math.PI / 2); + this.faceEntity.transform.roll = angle + Math.PI / 2; + }, + + touch: function (p) { this.inputs.mousemove.call(this, p); }, + + tap: function (p) { + this.disappear(); + this.disappear = yf.I; + p = this.layer0.worldFromDevice(p); + this.faceEntity.attach(new yuu.Animation(JUMP, { + $: this.faceEntity.transform, + target: [p.x, p.y] + })); + }, + }, +}); + +function load () { + yuu.director.pushScene(new FollowScene()); +} + +window.addEventListener("load", function() { + yuu.registerInitHook(load); + yuu.init({ backgroundColor: [1, 1, 1, 0] }) + .then(function () { yuu.director.start(); }); +}); + diff --git a/src/yuu/audio.js b/src/yuu/audio.js new file mode 100644 index 0000000..881924e --- /dev/null +++ b/src/yuu/audio.js @@ -0,0 +1,524 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + yuu.Audio = yT({ + /** Audio context/source/buffer accessor + + You probably don't need to make this yourself; one is made + named yuu.audio during initialization. + + You can set the master volume with yuu.audio.masterVolume. + */ + constructor: function () { + this._ctx = new window.AudioContext(); + this._compressor = this._ctx.createDynamicsCompressor(); + this._masterVolume = this._ctx.createGain(); + this._masterVolume.connect(this._compressor); + this._compressor.connect(this._ctx.destination); + this._musicVolume = this._ctx.createGain(); + this._musicVolume.connect(this._masterVolume); + this._masterVolume.gain.value = 0.5; + this._musicVolume.gain.value = 0.5; + + this._bufferCache = {}; + this._mute = false; + this._storage = null; + this._volume = this._masterVolume.gain.value; + }, + + destination: { alias: "_masterVolume", readonly: true }, + music: { alias: "_musicVolume", readonly: true }, + + _readStorage: function () { + if (!this._storage) + return; + yf.each.call(this, function (prop) { + this[prop] = this._storage.getObject(prop, this[prop]); + }, ["volume", "musicVolume", "mute"]); + }, + + _writeStorage: yf.debounce(function () { + if (!this._storage) + return; + yf.each.call(this, function (prop) { + this._storage.setObject(prop, this[prop]); + }, ["volume", "musicVolume", "mute"]); + }), + + storage: { + get: function () { return this._storage; }, + set: function (v) { + this._storage = v; + this._readStorage(); + } + }, + + mute: { + get: function () { return this._mute; }, + set: function (v) { + this._mute = !!v; + this.volume = this.volume; + } + }, + + volume: { + get: function () { return this._volume; }, + set: function (v) { + this._volume = v; + v = this._mute ? 0 : v; + this._masterVolume.gain.value = v; + this._writeStorage(); + } + }, + + musicVolume: { + get: function () { return this._musicVolume.gain.value; }, + set: function (v) { + this._musicVolume.gain.value = v; + this._writeStorage(); + } + }, + + currentTime: { alias: "_ctx.currentTime" }, + + decodeAudioData: function (data) { + var ctx = this._ctx; + try { + return ctx.decodeAudioData(data); + } catch (exc) { + return new Promise(function (resolve, reject) { + ctx.decodeAudioData(data, function (buffer) { + resolve(buffer); + }, function () { + reject(new Error("Error decoding audio buffer")); + }); + }); + } + }, + + createBufferSource: function (path) { + var source = this._ctx.createBufferSource(); + var sample = new yuu.AudioSample(path, this); + if ((source.buffer = sample.buffer) === null) { + sample.ready.then(function () { + source.buffer = sample.buffer; + }); + } + return source; + }, + + sampleRate: { alias: "_ctx.sampleRate" }, + createGain: { proxy: "_ctx.createGain" }, + createOscillator: { proxy: "_ctx.createOscillator" }, + }); + + // FIXME: This parsing is garbagey, would be better to parse when + // first handed a dfn and turn everything into a function. + function applyMod (s, v) { + if (yf.isFunction(s)) + return s(v); + else if (s === +s) + return s; + else if (s[0] === "-" || s[0] === "+") + return v + (+s); + else if (s[0] === "x" || s[0] === "*") + return v * +s.substring(1); + else if (s[s.length - 1] === "%") + return v * (parseFloat(s) / 100); + else + return +s; + } + + var Envelope = yuu.Envelope = yT({ + constructor: yf.argcd( + function (pairs) { + Envelope.call(this, pairs, 1); + }, + function (pairs, scale) { + pairs = pairs || { "0": 1, "100%": 1 }; + this.ts = Object.keys(pairs); + this.vs = yf.map.call(pairs, yf.getter, this.ts); + this.scale = scale; + var a = 0, b = 0; + var unlimited = false; + yf.each(function (t) { + if (+t) { + a = Math.max(+t, a); + b = Math.min(+t, b); + } + unlimited = unlimited || (t[t.length - 1] === "%"); + }, this.ts); + this.minDuration = a - b; + this.maxDuration = (unlimited || a === b) + ? Infinity + : this.minDuration; + var vMin = Math.min.apply(Math, this.vs); + var vMax = Math.max.apply(Math, this.vs); + this.constant = vMin === vMax && this.vs[0] * this.scale; + } + ), + + schedule: function (param, t0, scale, duration) { + if (this.constant !== false) { + param.setValueAtTime(scale * this.constant, t0); + } else { + yf.each.call(this, function (s, v) { + v = v * scale * this.scale; + var t = t0 + applyMod(s, duration); + if (t === t0) + param.setValueAtTime(v, t); + else + param.linearRampToValueAtTime(v, t); + }, this.ts, this.vs); + } + } + }); + + yuu.AudioSample = yuu.Caching(yT({ + constructor: function (path, ctx) { + ctx = ctx || yuu.audio; + var url = yuu.resourcePath(path, "sound", "wav"); + this.data = null; + this.ready = yuu.GET(url, { responseType: "arraybuffer" }) + .then(ctx.decodeAudioData.bind(ctx)) + .then(yf.setter.bind(this, "buffer")) + .then(yf.K(this)); + } + }), function (args) { return args.length <= 2 ? args[0] : null; }); + + yuu.Modulator = yT({ + constructor: function (dfn) { + this.envelope = new yuu.Envelope(dfn.envelope); + this.frequency = dfn.frequency; + this.index = dfn.index || 1.0; + }, + + createModulator: function (ctx, t0, fundamental, duration) { + var modulator = ctx.createOscillator(); + modulator.frequency.value = applyMod( + this.frequency, fundamental); + modulator.start(t0); + modulator.stop(t0 + duration); + var modulatorG = ctx.createGain(); + modulator.connect(modulatorG); + this.envelope.schedule( + modulatorG.gain, t0, this.index * fundamental, duration); + return modulatorG; + } + }); + + yuu.Instrument = yT({ + constructor: function (dfn) { + if (yf.isString(dfn)) { + var sampleName = dfn; + dfn = { sample: {} }; + dfn.sample[sampleName] = {}; + } + this.envelope = new yuu.Envelope(dfn.envelope); + this.frequency = dfn.frequency || (dfn.sample ? {} : { "x1": 1.0 }); + this.modulator = yf.map( + yf.new_(yuu.Modulator), yf.arrayify(dfn.modulator || [])); + this.sample = dfn.sample || {}; + this.ready = yuu.ready( + yf.map(yf.new_(yuu.AudioSample), Object.keys(this.sample)), + this); + }, + + createSound: function (ctx, t0, fundamental, amplitude, duration) { + // TODO: In the case of exactly one sample with a constant + // envelope, optimize out the extra gain node. + duration = yf.clamp(duration || 0, + this.envelope.minDuration, + this.envelope.maxDuration); + var ret = ctx.createGain(); + var dst = ret; + + yf.ipairs(function (name, params) { + var buffer = new yuu.AudioSample(name).buffer; + if (buffer && !params.loop) + duration = Math.max(buffer.duration, duration); + }, this.sample); + + var modulators = yf.map(function (modulator) { + return modulator.createModulator( + ctx, t0, fundamental, duration); + }, this.modulator); + + yf.ipairs.call(this, function (name, params) { + var src = ctx.createBufferSource(name); + src.loop = params.loop || false; + src.playbackRate.value = applyMod( + params.playbackRate || 1, fundamental || ctx.sampleRate); + yf.each(function (mod) { mod.connect(src.playbackRate); }, + modulators); + if (params.duration) + src.start(t0, params.offset || 0, params.duration); + else + src.start(t0, params.offset || 0); + src.stop(t0 + duration); + src.connect(dst); + }, this.sample); + + yf.ipairs.call(this, function (mfreq, mamp) { + var osc = ctx.createOscillator(); + osc.frequency.value = applyMod(mfreq, fundamental); + osc.start(t0); + osc.stop(t0 + duration); + yf.each(function (mod) { mod.connect(osc.frequency); }, + modulators); + if (mamp !== 1) { + var gain = ctx.createGain(); + gain.gain.value = mamp; + osc.connect(gain); + gain.connect(dst); + } else { + osc.connect(dst); + } + }, this.frequency); + + ret.gain.value = 0; + this.envelope.schedule(ret.gain, t0, amplitude, duration); + return ret; + }, + + play: yf.argcd( + function () { + return this.play(null, 0, 0, 1, 1); + }, + function (ctx, t, freq, amp, duration) { + ctx = ctx || yuu.audio; + t = t || ctx.currentTime; + var g = this.createSound(ctx, t, freq, amp, duration); + g.connect(ctx.destination); + return g; + } + ) + }); + + yuu.Instruments = yf.mapValues(yf.new_(yuu.Instrument), { + SINE: { + envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 }, + }, + + ORGAN: { + envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 }, + frequency: { "x1": 0.83, "x1.5": 0.17 } + }, + + SIREN: { + envelope: { "0": 1 }, + modulator: { + envelope: { "0": 1 }, + frequency: "1", + index: 0.2 + } + }, + + BELL: { + envelope: { "0": 1, "2.5": 0.2, "5": 0 }, + modulator: { + envelope: { "0": 1, "2.5": 0.2, "5": 0 }, + frequency: "x1.5", + } + }, + + BRASS: { + envelope: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 }, + modulator: { + envelope: { "0": 0, "0.2": 1, "0.4": 0.6, + "-0.1": 0.5, "100%": 0 }, + frequency: "x1", + index: 5.0 + } + }, + }); + + // Tune to A440 by default, although every interface should provide + // some ways to work around this. This gives C4 = ~261.63 Hz. + // https://en.wikipedia.org/wiki/Scientific_pitch_notation + yuu.C4_HZ = 440 * Math.pow(2, -9/12); + + yuu.Scale = yT({ + constructor: function (intervals) { + this.intervals = intervals; + this.length = this.intervals.length; + this.span = yf.foldl(function (a, b) { return a + b; }, intervals); + }, + + hz: function (tonic, degree, accidental) { + accidental = accidental || 0; + var s = this.span * ((degree / this.intervals.length) | 0) + + accidental; + degree %= this.intervals.length; + if (degree < 0) { + degree += this.intervals.length; + s -= this.span; + } + var i = 0; + while (degree >= 1) { + degree -= 1; + s += this.intervals[i]; + i++; + } + if (degree > 0) + s += this.intervals[i] * degree; + return tonic * Math.pow(2, s / 1200.0); + } + }); + + yuu.Scales = yf.mapValues(yf.new_(yuu.Scale), { + CHROMATIC: yf.repeat(100, 12), + MINOR: [200, 100, 200, 200, 100, 200, 200], + MAJOR: [200, 200, 100, 200, 200, 200, 100], + WHOLE_TONE: [200, 200, 200, 200, 200, 200], + AUGMENTED: [300, 100, 300, 100, 300, 100], + _17ET: yf.repeat(1200 / 17, 17), + DOUBLE_HARMONIC: [100, 300, 100, 200, 100, 300, 100], + }); + + var DURATION = { T: 1/8, S: 1/4, I: 1/2, Q: 1, H: 2, W: 4, + ".": 1.5, "/": 1/3, "<": -1 }; + var ACCIDENTAL = { b: -1, "#": 1, t: 0.5, d: -0.5 }; + + var NOTE = /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g; + + var LETTERS = { Z: null, X: null }; + + yuu.parseNote = function (note, scale, C4) { + return (C4 || yuu.C4_HZ) * Math.pow(2, LETTERS[note] / 12); + }; + + yuu.parseScore = function (score, scale, tonic, C4) { + // Note language: + // + // To play a scientific pitch note and advance the time, just + // use its name: G4, Cb2, A#0 + // + // To adjust the length of the note, use T, S, I, Q (default), + // H, W for 32nd through whole. Append . to do + // time-and-a-half. Append / to cut into a third. Append < to + // go back in time. + // + // To play a note on the provided scale, use a 0-based number + // (which can be negative). To move the current scale up or + // down, use + or -. For example, in C major, 0 and C4 produce + // the same note; after a -, 0 and C3 produce the same note. + // + // To rest, use Z or X. + // + // To play multiple notes at the same time, enclose them all with + // < ... >. The time will advance in accordance with the shortest + // one. + // + // To reset the time, scale offset, and duration, use a {. + // This can be more convenient when writing pieces with + // multiple parts than grouping, e.g. + // H < 1 8 > < 2 7 > < 3 6 > < 4 5 > + // is easier to understand when split into multiple lines: + // H 1 2 3 4 + // { H 8 7 6 5 + + scale = scale || yuu.Scales.MAJOR; + C4 = C4 || yuu.C4_HZ; + tonic = tonic || scale.tonic || C4; + if (yf.isString(tonic)) + tonic = yuu.parseNote(tonic, C4); + + var t = 0; + var notes = []; + var degree = 0; + var groupLength = 0; + var defaultDuration = "Q"; + var match; + + function calcDuration (d, m) { return d * DURATION[m]; } + function calcAccidental (d, m) { return d * ACCIDENTAL[m]; } + + while ((match = NOTE.exec(score))) { + switch (match[5]) { + case "<": + groupLength = Infinity; + break; + case ">": + t += groupLength === Infinity ? 0 : groupLength; + groupLength = 0; + break; + case "+": + degree += scale.length; + break; + case "-": + degree -= scale.length; + break; + case "{": + t = 0; + degree = 0; + groupLength = 0; + defaultDuration = "Q"; + break; + default: + if (match[5]) { + defaultDuration = match[5]; + continue; + } + var letter = match[2]; + var duration = yf.foldl( + calcDuration, match[1] || defaultDuration, 1); + if (LETTERS[letter] !== null) { + var offset = match[3]; + var accidental = yf.foldl( + calcAccidental, match[4] || "", 0) * 100; + notes.push({ + time: t, + duration: duration, + hz: letter + ? C4 * Math.pow(2, LETTERS[letter]/12.0) + : scale.hz(tonic, degree + (+offset || 0), accidental) + }); + } + if (groupLength && duration > 0) + groupLength = Math.min(groupLength, duration); + else + t += duration; + } + } + + notes.sort(function (a, b) { return a.time - b.time; }); + return notes; + }; + + yf.irange.call(LETTERS, function (i) { + yf.ipairs.call(this, function (l, o) { + var b = o + 12 * (i - 4); + this[l + i] = b; + yf.ipairs.call(this, function (s, m) { + this[l + s + i] = b + m; + }, ACCIDENTAL); + }, { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }); + }, 11); + + yuu.registerInitHook(function () { + if (!window.AudioContext) + throw new Error("Web Audio isn't supported."); + yuu.audio = new yuu.Audio(); + yuu.defaultCommands.volume = yuu.propcmd( + yuu.audio, "volume", + "get/set the current master audio volume", "0...1"); + yuu.defaultCommands.musicVolume = yuu.propcmd( + yuu.audio, "musicVolume", + "get/set the current music volume", "0...1"); + yuu.defaultCommands.mute = yuu.propcmd( + yuu.audio, "mute", "mute or unmute audio"); + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/ce.js b/src/yuu/ce.js new file mode 100644 index 0000000..bb5c6e5 --- /dev/null +++ b/src/yuu/ce.js @@ -0,0 +1,646 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + /** yuu-ce - entity/component system for the Yuu engine + + Game logic in Yuu is implemented via entities and components. + Entities (yuu.E) represent "things" in the world, and + components (yuu.C) individual properties or abilities of those + things. By attaching and detaching components, entities gain + and lose those abilities. + + This system prioritizes for convenience and simplicity over + performance. Many common optimizations in E/C systems, like + component pooling and array-of-structures, are not + implemented. (And are of questionable value in a language like + JavaScript.) + */ + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + yuu.E = yT({ + constructor: function () { + /** Entity, a shell to customize with components + + Entities exist as an aggregate of components (yuu.C). + They are used for components to talk to each other, or + other systems to handle systems in aggregate without + caring about the underlying details. + + Entities expose components in two ways. + + First, components may have one or more slots; when a + component is attached to an entity it slots itself + into those properties on that entity. For example, if + you attach a Transform component to an entity, you can + retrieve it via e.transform. + + Second, components may have one or more message + taps. This allows them to listen for, and respond to, + messages sent to the entity. Unlike slots many + attached components may have the same tap. + */ + this.parent = null; + this.children = []; + this.taps = {}; + this.attach.apply(this, arguments); + }, + + addChild: function (child) { this.addChildren(child); }, + removeChild: function (child) { this.removeChildren(child); }, + + addChildren: function () { + yf.stash("parent", this, arguments); + this.children = this.children.concat(yf.slice(arguments)); + }, + + removeChildren: function () { + this.children = yf.filter( + yf.lacks.bind(null, arguments), this.children); + yf.stash("parent", null, arguments); + }, + + attach: function () { + /** Attach a component to this entity. + + If the entity already has a component in the same slots, + an error will be thrown. + */ + for (var j = 0; j < arguments.length; ++j) { + var c = arguments[j]; + var i; + for (i = 0; i < c.SLOTS.length; ++i) + if (this[c.SLOTS[i].slot]) + throw new Error("Entity already has a " + c.SLOTS[i]); + for (i = 0; i < c.SLOTS.length; ++i) + this[c.SLOTS[i]] = c; + for (i = 0; i < c.TAPS.length; ++i) + this.taps[c.TAPS[i]] = (this.taps[c.TAPS[i]] || []) + .concat(c); + c.entity = this; + c.attached(this); + } + }, + + detach: function () { + /** Detach a component from this entity */ + for (var j = 0; j < arguments.length; ++j) { + var c = arguments[j]; + var i; + for (i = 0; i < c.SLOTS.length; ++i) + if (this[c.SLOTS[i].slot] !== c) + throw new Error("Entity has a wrong " + c.SLOTS[i]); + for (i = 0; i < c.SLOTS.length; ++i) + delete this[c.SLOTS[i]]; + for (i = 0; i < c.TAPS.length; ++i) + this.taps[c.TAPS[i]] = yf.without(this.taps[c.TAPS[i]], c); + c.entity = null; + c.detached(this); + } + }, + + _message: function (name, params) { + var taps = this.taps[name]; + var children = this.children; + var i; + if (taps) + for (i = 0; i < taps.length; ++i) + taps[i][name].apply(taps[i], params); + for (i = 0; i < children.length; ++i) + children[i]._message(name, params); + }, + message: function (name) { + /** Message components listening on the named tap */ + this._message(name, yf.tail(arguments)); + }, + }); + + yuu.C = yT({ + entity: { value: null, writable: true }, + SLOTS: { value: [], configurable: true }, + TAPS: { value: [], configurable: true }, + + attached: function (entity) { }, + detached: function (entity) { }, + }); + + yuu.DataC = yT(yuu.C, { + /** A component for random scratch data + + Storing this in a separate component rather than on the + entity directly reduces the chance of naming conflicts and + also the number of hidden classes. + */ + + constructor: function (data) { + Object.assign(this, data || {}); + }, + + SLOTS: ["data"] + }); + + + yuu.Animation = yT(yuu.C, { + constructor: function (timeline, params, completionHandler, delay) { + this.timeline = yf.mapValues(yf.arrayify, timeline); + this.params = params; + this.completionHandler = completionHandler; + this.keys = Object.keys(timeline) + .sort(function (a, b) { + return +this._lookup(a) - +this._lookup(b); + }.bind(this)); + this._t1 = +this._lookup(yf.last(this.keys)) + 1; + this._t = -(delay || 0); + this._pc = 0; + this._tweens = []; + }, + + attached: function () { + this.tick(); + }, + + _lookup: function (k) { + return (k in this.params) ? this.params[k] : k; + }, + + set1: function (setter) { + var $ = this.params.$; + yf.ipairs.call(this, function (k, v) { + $[k] = this._lookup(v); + }, setter); + }, + + set: function (setters) { + yf.ipairs.call(this, function (name, setter) { + var $ = this._lookup(name); + yf.ipairs.call(this, function (k, v) { + $[k] = this._lookup(v); + }, setter); + }, setters); + }, + + _addTween: function (tweens, instr) { + var repeat = instr.repeat || 0; + var cycles = Math.abs(repeat) + 1; + var easing = yf.isFunction(instr.easing) + ? instr.easing + : yuu.Tween[(instr.easing || "ease").toUpperCase()]; + var duration, complete; + + if ("complete" in instr) { + complete = this._lookup(instr.complete); + duration = (complete - this._t) / cycles; + } else if ("duration" in instr) { + duration = this._lookup(instr.duration); + complete = this._t + duration * cycles; + } + + if (isFinite(cycles)) { + this._tweens.push( + new yuu.Tween(tweens, duration, repeat, easing)); + this._t1 = Math.max(complete + 1, this._t1); + } else { + this.entity.attach(new yuu.TweenC( + tweens, duration, repeat, easing)); + } + }, + + tween1: function (tween, instr) { + var nt = { $: this._lookup(instr.$) || this.params.$ }; + yf.ipairs.call(this, function (k, v) { + nt[k] = [nt.$[k], this._lookup(v)]; + }, tween); + this._addTween([nt], instr); + }, + + tween: function (targets, instr) { + var tweens = []; + yf.ipairs.call(this, function (name, tween) { + var nt = { $: this._lookup(name) || this.params.$ }; + yf.ipairs.call(this, function (k, v) { + nt[k] = [nt.$[k], this._lookup(v)]; + }, tween); + tweens.push(nt); + }, targets); + this._addTween(tweens, instr); + }, + + tweenAll: function (tween, instr) { + var tweens = []; + var $s = this._lookup(instr.$s) || this.params.$s; + yf.irange.call(this, function (i) { + var nt = { $: $s[i] }; + yf.ipairs.call(this, function (k, v) { + nt[k] = [nt.$[k], this._lookup(v)[i]]; + }, tween); + tweens.push(nt); + }, $s.length); + this._addTween(tweens, instr); + }, + + event: function (name) { + this.params[name](this, this.params); + }, + + _dispatch: function (instr) { + if (instr.set1) + this.set1(instr.set1); + if (instr.set) + this.set(instr.set); + if (instr.tween1) + this.tween1(instr.tween1, instr); + if (instr.tween) + this.tween(instr.tween, instr); + if (instr.tweenAll) + this.tweenAll(instr.tweenAll, instr); + if (instr.event) + this.event(instr.event); + }, + + tick: function () { + var t = this._t; + var i; + for (var key = this.keys[this._pc]; + this._lookup(key) <= t; + key = this.keys[++this._pc]) { + yf.each.call(this, this._dispatch, this.timeline[key]); + } + + for (i = this._tweens.length - 1; i >= 0; --i ) { + if (this._tweens[i].tick()) + this._tweens.splice(i, 1); + } + + if (++this._t > this._t1) { + if (this.completionHandler) + this.completionHandler(this); + this.entity.detach(this); + } + }, + + tock: function (p) { + for (var i = this._tweens.length - 1; i >= 0; --i) + this._tweens[i].tock(p); + }, + + TAPS: ["tick", "tock"] + }); + + yuu.Tween = yT({ + /** Tween object properties over time + + This component changes properties over time, and can + handle synchronizing multiple objects and multiple + properties. + + The `property` is either a single object with the special + `$` property set to the object to tween and every other + property set to the properties to tween with values [min, + max], or a list of such objects. For example, to tween a.x + from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you + would pass + + [{ $: a, x: [0, 1], y: [2, 3] }, + { $: b, z: [1, 2] }] + + The `duration` is specified in ticks (e.g. calls to + director.tick). + + `repeat` may be a positive number to repeat the tween that + many times, or a negative number to cycle back to the + minimum (and then back to the maximum, etc.) that many + times. `Infinity` will repeat the tween forever and + `-Infinity` will cycle the tween back and forth forever. + + A custom easing equation may be provided. This is a + function which takes a p = [0, 1] and returns the eased p. + */ + + constructor: function (props, duration, repeat, easing) { + this._object = []; + this._property = []; + this._a = []; + this._b = []; + this._count = 0; + this.duration = duration || 60; + this.repeat = repeat || 0; + this.easing = easing || yuu.Tween.LINEAR; + yf.each.call(this, function (oab) { + yf.ipairs.call(this, function (name, ab) { + if (name !== "$") { + this._object.push(oab.$); + this._property.push(name); + this._a.push(ab[0]); + this._b.push(ab[1]); + } + }, oab); + }, yf.arrayify(props)); + this._updateAt(0); + }, + + tick: function () { + var t = this._count / this.duration; + ++this._count; + if (t > 1 && !this.repeat) + return true; + else if (t >= 1 && this.repeat) { + if (this.repeat < 0) { + var n = this._a; + this._a = this._b; + this._b = n; + } + this._count = 1; + t = 0; + + if (this.repeat < 0) { + this.repeat++; + } else if (this.repeat > 0) { + this.repeat--; + } + } + this._updateAt(t); + }, + + tock: function (p) { + var t = (this._count + p - 1) / this.duration; + if (t <= 1) + this._updateAt(t); + }, + + _updateAt: function (t) { + var p = this.easing ? this.easing(t) : t; + for (var i = 0; i < this._object.length; ++i) { + // a was the existing property, b was the one provided + // by the user. By lerping from b to a, the user can + // control the lerp type in some awkward cases - + // e.g. CSS DOM values are all exposed as strings so a + // will be a string/String, but if b is provided as a + // number/Number, this will lerp numerically. + // + // FIXME: This still doesn't work right if the lerp is + // later reversed due to negative repeats. + var object = this._object[i]; + var property = this._property[i]; + var a = this._a[i]; + var b = this._b[i]; + object[property] = yuu.lerp(b, a, 1 - p); + } + }, + + count: { + get: function () { return this._count; }, + set: function (v) { this._count = Math.round(v); }, + }, + + duration: { value: 60, chainable: true }, + repeat: { value: 0, chainable: true }, + easing: { value: null, chainable: true }, + }); + + yuu.TweenC = yT(yuu.C, { + constructor: function () { + this._tween = yf.construct(yuu.Tween, arguments); + }, + + tick: function () { + if (this._tween.tick()) + this.entity.detach(); + }, + + tock: { proxy: "_tween.tock" }, + count: { alias: "_tween.count" }, + duration: { alias: "_tween.duration", chainable: true }, + repeat: { alias: "_tween.repeat", chainable: true }, + easing: { alias: "_tween.easing", chainable: true }, + + TAPS: ["tick", "tock"] + }); + + yuu.Tween.LINEAR = null; + /** No easing */ + + yuu.Tween.EASE = function (p) { + /** Ease in and out + + This equation is from _Improving Noise_ (Perlin, 2002). It + is symmetrical around p=0.5 and has zero first and second + derivatives at p=0 and p=1. + */ + return p * p * p * (p * (p * 6.0 - 15.0) + 10.0); + }; + + yuu.Tween.EASE_IN = function (p) { + return p * p * p; + }; + + yuu.Tween.METASPRING = function (amplitude, pulsation) { + /** A generator for springy tweens + + The amplitude controls how far from the final position the + spring will bounce, as a multiple of the distance between the + start and end. A "normal" amplitude is around 0.5 to 1.5. + + The pulsation constant controls the rigidity of the spring; + higher pulsation results in a spring that bounces more quickly + and more often during a fixed interval. A "normal" pulsation + constant is around 15 to 30. + */ + return function (p) { + return 1 + Math.cos(pulsation * p + Math.PI) * (1 - p) * amplitude; + }; + }; + + yuu.Tween.STEPPED = function (segments, alpha) { + return function (p) { + p = p * segments; + var lower = Math.floor(p); + var upper = Math.floor((p + alpha)); + if (upper > lower) { + var p1 = 1 - (upper - p) / alpha; + return (lower + p1) / segments; + } else { + return lower / segments; + } + }; + }; + + yuu.Transform = yT(yuu.C, { + /** A 3D position, rotation (as quaternion), and scale + + This also serves as an object lesson for a simple slotted + component. + */ + constructor: function (position, rotation, scale) { + this._position = vec3.clone(position || [0, 0, 0]); + this._rotation = quat.clone(rotation || [0, 0, 0, 1]); + this._scale = vec3.clone(scale || [1, 1, 1]); + this._matrix = mat4.create(); + this._dirty = true; + this._version = 0; + this._parentVersion = null; + }, + + SLOTS: ["transform"], + + position: { + chainable: true, + get: function () { return this._position.slice(); }, + set: function (v) { this._dirty = true; + vec3.copy(this._position, v); } + }, + rotation: { + chainable: true, + get: function () { return this._rotation.slice(); }, + set: function (v) { this._dirty = true; + quat.normalize(this._rotation, v); } + }, + scale: { + chainable: true, + get: function () { return this._scale.slice(); }, + set: function (v) { this._dirty = true; + vec3.copy(this._scale, v); } + }, + x: { + chainable: true, + get: function () { return this._position[0]; }, + set: function (x) { this._dirty = true; this._position[0] = x; } + }, + y: { + chainable: true, + get: function () { return this._position[1]; }, + set: function (x) { this._dirty = true; this._position[1] = x; } + }, + z: { + chainable: true, + get: function () { return this._position[2]; }, + set: function (x) { this._dirty = true; this._position[2] = x; } + }, + xy: { swizzle: "xy", chainable: true }, + + scaleX: { + chainable: true, + get: function () { return this._scale[0]; }, + set: function (x) { this._dirty = true; this._scale[0] = x; } + }, + scaleY: { + chainable: true, + get: function () { return this._scale[1]; }, + set: function (x) { this._dirty = true; this._scale[1] = x; } + }, + scaleZ: { + chainable: true, + get: function () { return this._scale[2]; }, + set: function (x) { this._dirty = true; this._scale[2] = x; } + }, + + worldToLocal: function (p) { + var x = (p.x || p[0] || 0); + var y = (p.y || p[1] || 0); + var z = (p.z || p[2] || 0); + var local = [x, y, z]; + var matrix = mat4.clone(this.matrix); + return vec3.transformMat4(local, local, mat4.invert(matrix, matrix)); + }, + + contains: function (p) { + p = this.worldToLocal(p); + return p[0] >= -0.5 && p[0] < 0.5 + && p[1] >= -0.5 && p[1] < 0.5 + && p[2] >= -0.5 && p[2] < 0.5; + }, + + ypr: { + chainable: true, + get: function () { + var q = this._rotation; + var x = q[0]; var sqx = x * x; + var y = q[1]; var sqy = y * y; + var z = q[2]; var sqz = z * z; + var w = q[3]; + var abcd = w * x + y * z; + if (abcd > 0.499) + return [2 * Math.atan2(x, w), Math.PI / 2, 0]; + else if (abcd < -0.499) + return [-2 * Math.atan2(x, w), -Math.PI / 2, 0]; + else { + var adbc = w * z - x * y; + var acbd = w * y - x * z; + return [Math.atan2(2 * adbc, 1 - 2 * (sqz + sqx)), + Math.asin(2 * abcd), + Math.atan2(2 * acbd, 1 - 2 * (sqy + sqx))]; + } + + }, + set: function (ypr) { + var q = this._rotation; + quat.identity(q); + quat.rotateZ(q, q, ypr[0]); + quat.rotateY(q, q, ypr[2]); + quat.rotateX(q, q, ypr[1]); + this._dirty = true; + } + }, + + yaw: { aliasSynthetic: "ypr[0]", chainable: true }, + pitch: { aliasSynthetic: "ypr[1]", chainable: true }, + roll: { aliasSynthetic: "ypr[2]", chainable: true }, + + matrix: { + get: function () { + var pt = this.entity.parent && this.entity.parent.transform; + var pm = pt && pt.matrix; + var ptVersion = pt && pt._version; + if (this._dirty || (ptVersion !== this._parentVersion)) { + var m = this._matrix; + mat4.identity(m); + mat4.fromRotationTranslation( + m, this._rotation, this._position); + mat4.scale(m, m, this._scale); + if (pm) + mat4.multiply(m, pm, m); + this._dirty = false; + this._matrix = m; + this._parentVersion = ptVersion; + this._version = (this._version + 1) | 0; + } + return this._matrix; + } + } + }); + + yuu.Ticker = yT(yuu.C, { + /** Set a callback to run every n ticks + + If the callback returns true, it is rescheduled for + execution (like setInterval). If it returns false, this + component is removed from the entity. + */ + constructor: function (callback, interval, delay) { + this.callback = callback; + this.interval = interval; + this._accum = 0; + this._count = -(delay || 0); + }, + + tick: function () { + this._accum += 1; + if (this._accum === this.interval) { + this._accum = 0; + if (!this.callback(this._count++)) + this.entity.detach(this); + } + }, + + TAPS: ["tick"] + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/core.js b/src/yuu/core.js new file mode 100644 index 0000000..cbeb2a0 --- /dev/null +++ b/src/yuu/core.js @@ -0,0 +1,908 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + + yuu.require = function (m) { + try { return require(m); } + catch (exc) { return null; } + }; + + if (!Math.sign) + require("./pre"); + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + var gui = yuu.require("nw.gui"); + var fs = yuu.require("fs"); + var stringLerp = this.stringLerp || yuu.require("string-lerp"); + + var initHooks = []; + var initOptions = null; + + if (typeof document !== "undefined") { + var scripts = document.getElementsByTagName('script'); + var path = yf.last(scripts).src.split('?')[0]; + yuu.PATH = path.split('/').slice(0, -1).join('/') + '/'; + } else { + yuu.PATH = "file://" + escape(module.__dirname) + "/"; + } + + yuu.registerInitHook = initHooks.push.bind(initHooks); + /** Register a hook to be called during Yuu initialization + + Hooks are called in registration order with the module and + the options dictionary passed to the init method. (This is + also set to the module.) + */ + + function showError (exc, kind) { + var prefix = "yuu-" + (kind || "") + "error"; + yuu.logError(exc); + var dialog = document.getElementById(prefix); + var errorMessage = document.getElementById(prefix + "-message"); + if (errorMessage) + errorMessage.textContent = exc.message; + var errorStack = document.getElementById(prefix + "-stack"); + if (errorStack) + errorStack.textContent = exc.message + "\n\n" + exc.stack; + return dialog; + } + yuu.showError = showError; + + function fatalError (exc) { + var dialog = showError(exc, "fatal-"); + if (dialog) + dialog.style.display = "block"; + if (gui) { + gui.Window.get().show(); + gui.Window.get().focus(); + } + throw exc; + } + + yuu.init = function (options) { + /** Initialize Yuu and call all registered hooks + */ + + document.body.className += (navigator.standalone || gui) + ? " standalone" : " browser"; + + if (gui) { + var win = gui.Window.get(); + var nativeMenuBar = new gui.Menu({ type: "menubar" }); + if (nativeMenuBar.createMacBuiltin) { + nativeMenuBar.createMacBuiltin( + document.title, { hideEdit: true }); + win.menu = nativeMenuBar; + } + var wkdoc = document; + win.on("minimize", function () { + var ev = new Event("visibilitychange"); + wkdoc.hidden = true; + wkdoc.dispatchEvent(ev); + }); + win.on("restore", function () { + var ev = new Event("visibilitychange"); + wkdoc.hidden = false; + wkdoc.dispatchEvent(ev); + }); + win.on('new-win-policy', function (frame, url, policy) { + if (url.startsWith('chrome')) { + policy.forceNewPopup(); + } else { + policy.ignore(); + gui.Shell.openExternal(url); + } + }); + } + + return new Promise(function (resolve) { + // TODO: Some kind of loading progress bar. + initOptions = options || {}; + yuu.log("messages", "Initializing Yuu engine."); + var promises = []; + // initHooks can be pushed to while iterating, so iterate + // by index, not a foreach loop. + for (var i = 0; i < initHooks.length; ++i) + promises.push(initHooks[i].call(yuu, initOptions)); + initHooks = null; // Bust future registerInitHook calls. + yuu.log("messages", "Initialization hooks complete."); + if (gui) { + gui.Window.get().show(); + gui.Window.get().focus(); + } + resolve(Promise.all(yf.filter(null, promises))); + }).then(function () { + yuu.log("messages", "Loading complete."); + }).catch(fatalError); + }; + + yuu.log = yf.argv(function (category, args) { + /** Log a message to the console. + + This supports simple filtering by setting e.g. + `yuu.log.errors = true` to log anything with the + `"errors"` category. + */ + if (!category || this.log[category]) { + switch (category) { + case "errors": return console.error.apply(console, args); + case "warnings": return console.warn.apply(console, args); + default: return console.log.apply(console, args); + } + } + }); + + yuu.log.errors = true; + yuu.log.warnings = true; + yuu.log.messages = true; + + yuu.logError = function (e) { + yuu.log("errors", e.message || "unknown error", e); + }; + + yuu.GET = function (url, params) { + /** Promise the HTTP GET the contents of a URL. */ + return new Promise(function (resolve, reject) { + var req = new XMLHttpRequest(); + req.open("GET", url, true); + for (var k in params) + req[k] = params[k]; + req.onload = function () { + var status = this.status; + // status === 0 is given by node-webkit for success. + if ((status >= 200 && status < 300) || status === 0) + resolve(this.response); + else + reject(new Error( + url + ": " + status + ": " + this.statusText)); + }; + req.onabort = function () { reject(new Error("aborted")); }; + req.onerror = function () { reject(new Error("network error")); }; + req.ontimeout = function () { reject(new Error("timed out")); }; + req.send(null); + }); + }; + + yuu.Image = function (src) { + /** Promises a DOM Image. */ + return new Promise(function (resolve, reject) { + var img = new Image(); + img.onload = function () { + resolve(img); + }; + img.onerror = function () { + var msg = "Unable to load " + img.src; + yuu.log("errors", msg); + reject(new Error(msg)); + }; + img.src = src; + }); + }; + + /** Command parsing and execution + + The command API serves several roles. It is a way to enable or + disable different game logic within different scenes; capture + and replay or automate game events; loosely or late-bind game + modules; customize input mappings; and a debugging tool to + help inspect or modify the state of a running program. + + A command is a string of a command name followed by arguments + separated by whitespace. It's similar to a fully bound + closure. It is less flexible but easier to inspect, store, + replay, and pass around. + + Command names are mapped to functions, grouped into sets, and + the sets pushed onto a stack. They are executed by passing the + command string to the execute function which walks the stack + looking for the matching command. + + If the command string is prefaced with + or -, true or false + are appended to the argument list. e.g. `+command` is + equivalent to `command true` and `-command 1 b` is equivalent + to `command 1 b false`. By convention, commands of those forms + return their internal state when called with neither true or + false. This is useful for another special prefix, ++. When + called as `++command`, it is executed with no arguments, the + result inverted (with !), and then called again passing that + inverted value as the last argument. + */ + + function isCommand (f) { + return yf.isFunction(f) && f._isCommandFunction; + } + + function cmdbind () { + // Commands are practically a subtype of functions. Binding + // them (which happens often, e.g. when Scenes register + // commands) should also return a command. + var f = Function.prototype.bind.apply(this, arguments); + // usage is still valid iff no new arguments were given. + return cmd(f, arguments.length <= 1 && this.usage, this.description); + } + + var cmd = yuu.cmd = yf.argcd( + /** Decorate a function for command execution + + Command functions need some special attributes to work + correctly. This decorator makes sure they have them. + */ + function (f) { return yuu.cmd(f, null, null); }, + function (f, description) { return yuu.cmd(f, null, description); }, + function (f, usage, description) { + f._isCommandFunction = true; + f.usage = usage || " ".repeat(f.length).substring(1); + f.description = description || "no description provided"; + f.bind = cmdbind; + return f; + } + ); + + yuu.propcmd = function (o, prop, desc, valspec) { + /** Generate a command function that controls a property + + A common pattern for command functions is to simply get or + set a single object property. This wrapper will generate a + correct function to do that. + */ + valspec = valspec || typeof o[prop]; + desc = desc || "Retrieve or modify the value of " + prop; + return cmd(function () { + if (arguments.length) + o[prop] = arguments[0]; + return o[prop]; + }, "<" + valspec + "?>", desc); + }; + + var QUOTED_SPLIT = /[^"\s]+|"(?:\\"|[^"])+"/g; + var COMMAND_SPLIT = /\s+(&&|\|\||;)\s+/g; + + function parseParam (param) { + if (yf.head(param) === "{" && yf.last(param) === "}") + return resolvePropertyPath( + this, param.substr(1, param.length - 2)); + try { return JSON.parse(param); } + catch (exc) { return param; } + } + + function parseCommand (cmdstring, ctx) { + /** Parse a command string into an invocation object. + + The command string has a form like `+quux 1 2 3` or + `foobar "hello world"`. + + Multiple commands can be joined in one string with &&, ||, + or ;. To use these characters literally as a command + argument place them in quotes. + + Arguments wrapped in {}s are interpreted as property paths + for the provided context object. `{x[0].y}` will resolve + `ctx.x[0].y` and put that into the arguments array. To + avoid this behavior and get a literal string bounded by + {}, JSON-encode the string beforehand (e.g. `"{x[0].y}"`). + + The returned array contains objects with three properties: + `name` - the command name to execute + `args` - an array of objects to pass as arguments + `toggle` - if the command value should be toggled ('++') + and pushed into args + `cond` - "&&", "||", or ";", indicating what kind of + conditional should be applied. + */ + + var invs = []; + var conds = cmdstring.split(COMMAND_SPLIT); + for (var i = -1; i < conds.length; i += 2) { + var args = conds[i + 1].match(QUOTED_SPLIT).map(parseParam, ctx); + var name = args.shift(); + var toggle = false; + if (name[0] === "+" && name[1] === "+") { + name = name.substring(2); + toggle = true; + } else if (name[0] === "+") { + name = name.substring(1); + args.push(true); + } else if (name[0] === "-") { + name = name.substring(1); + args.push(false); + } + invs.push({ name: name, args: args, toggle: toggle, + cond: conds[i] || ";"}); + } + return invs; + } + + yuu.CommandStack = yT({ + constructor: function () { + /** A stack of command sets for command lookup and execution */ + this._cmdsets = yf.slice(arguments); + }, + + push: function (cmdset) { + /** Add a command set to the lookup stack. */ + this._cmdsets = this._cmdsets.concat(cmdset); + }, + + remove: function (cmdset) { + /** Remove a command set from the lookup stack. */ + this._cmdsets = yf.without(this._cmdsets, cmdset); + }, + + insertBefore: function (cmdset, before) { + this._cmdsets = yf.insertBefore( + this._cmdsets.slice(), cmdset, before); + }, + + execute: function (cmdstring, ctx) { + /* Execute a command given a command string. + + The command stack is searched top-down for the first + command with a matching name, and it is invoked. No + other commands are called. + + A command set may also provide a special function named + `$`. If no matching command name is found, this + function is called with the raw invocation object (the + result of yuu.parseCommand) and may return true to stop + processing as if the command had been found. + */ + var invs = parseCommand(cmdstring, ctx); + var cond; + var res; + yf.each.call(this, function (inv) { + if ((inv.cond === "&&" && !cond) || (inv.cond === "||" && cond)) + return; + if (!yf.eachrUntil(function (cmdset) { + var cmd = cmdset[inv.name]; + if (cmd) { + if (inv.toggle) + inv.args.push(!cmd.apply(null, inv.args)); + yuu.log("commands", "Executing:", inv.name, + inv.args.map(JSON.stringify).join(" ")); + res = cmd.apply(null, inv.args); + cond = res === undefined ? cond : !!res; + yuu.log("commands", "Result:", JSON.stringify(res)); + return true; + } + return cmdset.$ && cmdset.$(inv); + }, this._cmdsets)) + yuu.log("errors", "Unknown command", inv.name); + }, invs); + return res; + } + }); + + yuu.extractCommands = function (object) { + var commands = {}; + yf.each(function (prop) { + // Check the descriptor before checking the value, because + // checking the value of accessors (which should never be + // stable commands) is generally a bad idea during + // constructors, and command sets are often filled in during + // constructors. + if (yT.isDataDescriptor(yT.getPropertyDescriptor(object, prop)) + && isCommand(object[prop])) + commands[prop] = object[prop].bind(object); + }, yf.allKeys(object)); + return commands; + }; + + yuu.commandStack = new yuu.CommandStack(yuu.defaultCommands = { + /** The default command stack and set. */ + cmds: yuu.cmd(function (term) { + term = term || ""; + var cmds = []; + yuu.commandStack._cmdsets.forEach(function (cmdset) { + for (var cmdname in cmdset) { + if (cmdname.indexOf(term) >= 0) { + var cmd = cmdset[cmdname]; + var msg; + if (cmd.usage) + msg = [cmdname, cmd.usage, "--", cmd.description]; + else + msg = [cmdname, "--", cmd.description]; + cmds.push(msg.join(" ")); + } + } + }); + yuu.log("messages", cmds.join("\n")); + }, "", "display available commands (matching the term)"), + + echo: yuu.cmd(function () { + yuu.log("messages", arguments); + }, "...", "echo arguments to the console"), + + log: yuu.cmd(function (name, state) { + if (state !== undefined) + yuu.log[name] = !!state; + return yuu.log[name]; + }, " ", "enable/disable a logging category") + + }); + + yuu.defaultCommands.showDevTools = yuu.cmd(function () { + if (gui) + gui.Window.get().showDevTools(); + }, "show developer tools"); + + yuu.anchorPoint = function (anchor, x0, y0, x1, y1) { + /** Calculate the anchor point for a box given extents and anchor mode + + This function is the inverse of yuu.bottomLeft. + */ + switch (anchor) { + case "center": return [(x0 + x1) / 2, (y0 + y1) / 2]; + case "top": return [(x0 + x1) / 2, y1]; + case "bottom": return [(x0 + x1) / 2, y0]; + case "left": return [x0, (y0 + y1) / 2]; + case "right": return [x1, (y0 + y1) / 2]; + + case "topleft": return [x0, y1]; + case "topright": return [x1, y1]; + case "bottomleft": return [x0, y0]; + case "bottomright": return [x0, y0]; + default: return [anchor[0], anchor[1]]; + } + }; + + yuu.bottomLeft = function (anchor, x, y, w, h) { + /** Calculate the bottom-left for a box given size and anchor mode + + This function is the inverse of yuu.anchorPoint. + */ + switch (anchor) { + case "center": return [x - w / 2, y - h / 2]; + case "top": return [x - w / 2, y - h]; + case "bottom": return [x - w / 2, y]; + case "left": return [x, y - h / 2]; + case "right": return [x - w, y - h / 2]; + + case "topleft": return [x, y - h]; + case "topright": return [x - w, y - h]; + case "bottomleft": return [x, y]; + case "bottomright": return [x - w, y]; + default: return [anchor[0], anchor[1]]; + } + }; + + yuu.lerp = function (a, b, p) { + return (a !== null && a !== undefined && a.lerp) + ? a.lerp(b, p) : (b !== null && b !== undefined && b.lerp) + ? b.lerp(a, 1 - p) : p < 0.5 ? a : b; + }; + + yuu.bilerp = function (x0y0, x1y0, x0y1, x1y1, px, py) { + /** Bilinearly interpolate between four values in two dimensions */ + return yuu.lerp(yuu.lerp(x0y0, x1y0, px), yuu.lerp(x0y1, x1y1, px), py); + }; + + function resolvePropertyPath (object, path) { + /** Look up a full property path + + If a null is encountered in the path, this function returns + null. If undefined is encountered or a property is missing, it + returns undefined. + */ + var parts = path.replace(/\[(\w+)\]/g, '.$1').split('.'); + for (var i = 0; + i < parts.length && object !== undefined && object !== null; + ++i) { + object = object[parts[i]]; + } + return object; + } + + yuu.Random = yT({ + /** Somewhat like Python's random.Random. + + Passed a function that returns a uniform random variable in + [0, 1) it can do other useful randomization algorithms. + + Its methods are implemented straightforwardly rather than + rigorously - this means they may not behave correctly in + common edge cases like precision loss. + */ + constructor: function (generator) { + this.random = generator || Math.random; + this._spareGauss = null; + }, + + choice: function (seq) { + /** Return a random element from the provided array. */ + return seq[this.randrange(0, seq.length)]; + }, + + randrange: yf.argcd( + function (a) { + /** Return a uniform random integer in [0, a). */ + return (this.random() * a) | 0; + }, + + function (a, b) { + /** Return a uniform random integer in [a, b). */ + a = a | 0; + b = b | 0; + return a + ((this.random() * (b - a)) | 0); + }, + + function (a, b, step) { + /** Return a uniform random number in [a, b). + + The number is constrained to values of a + i * step + where i is a non-negative integer. + */ + var i = Math.ceil((b - a) / step); + return a + this.randrange(i) * step; + } + ), + + uniform: yf.argcd( + function (a) { + /** Return a uniform random variable in [0, a). */ + return a * this.random(); + }, + function (a, b) { + /** Return a uniform random variable in [a, b). */ + return a + (b - a) * this.random(); + } + ), + + gauss: function (mean, sigma) { + var u = this._spareGauss, v, s; + this._spareGauss = null; + + if (u === null) { + do { + u = this.uniform(-1, 1); + v = this.uniform(-1, 1); + s = u * u + v * v; + } while (s >= 1.0 || s === 0.0); + var t = Math.sqrt(-2.0 * Math.log(s) / s); + this._spareGauss = v * t; + u *= t; + } + return mean + sigma * u; + }, + + randbool: yf.argcd( + /** Return true the given percent of the time (default 50%). */ + function () { return this.random() < 0.5; }, + function (a) { return this.random() < a; } + ), + + randsign: function (v) { + return this.randbool() ? v : -v; + }, + + shuffle: function (seq) { + for (var i = seq.length - 1; i > 0; --i) { + var index = this.randrange(i + 1); + var temp = seq[i]; + seq[i] = seq[index]; + seq[index] = temp; + } + return seq; + }, + + discard: function (z) { + z = z | 0; + while (z-- > 0) + this.random(); + } + }); + + yuu.createLCG = yf.argcd( + /** Linear congruential random generator + + This returns a function that generates numbers [0, 1) as + with Math.random. You can also read or assign the `state` + attribute to set the internal state. + */ + function () { return yuu.createLCG(Math.random() * 2147483647); }, + function (seed) { + var state = seed | 0; + return function generator () { + state = (state * 1664525 + 1013904223) % 4294967296; + return state / 4294967296; + }; + } + ); + + yuu.random = new yuu.Random(); + + function defaultKey (args) { + // Cache things that can be constructed with one string. + return args.length === 1 && yf.isString(args[0]) ? args[0] : null; + } + + yuu.Caching = function (Type, cacheKey) { + function ctor () { + var k = ctor._cacheKey(arguments); + var o = k && ctor._cache[k]; + if (!o) + o = ctor._cache[k] = yf.construct(ctor.Uncached, arguments); + return o; + } + ctor._cacheKey = cacheKey || defaultKey; + ctor._cache = {}; + ctor.Uncached = Type; + return ctor; + }; + + yuu.transpose2d = function (a) { + for (var x = 0; x < a.length; ++x) { + for (var y = 0; y < x; ++y) { + var t = a[x][y]; + a[x][y] = a[y][x]; + a[y][x] = t; + } + } + }; + + yuu.normalizeRadians = function (theta) { + var PI = Math.PI; + return (theta + 3 * PI) % (2 * PI) - PI; + }; + + yuu.radians = function (v) { + return v * (Math.PI / 180.0); + }; + + yuu.degrees = function (v) { + return v * (180.0 / Math.PI); + }; + + var SHORT = /(\/|^)@(.+)$/; + yuu.resourcePath = function (path, category, ext) { + var match; + if ((match = path.match(SHORT))) { + path = path.replace(/^yuu\/@/, yuu.PATH + "@") + .replace(SHORT, "$1data/" + category + "/$2"); + if (match[2].indexOf(".") === -1) + path += "." + ext; + } + return path; + }; + + yuu.ready = function (resources, result) { + return Promise.all(yf.filter(null, yf.pluck("ready", resources))) + .then(yf.K(result)); + }; + + yuu.openURL = function (url) { + if (gui && gui.Shell) + gui.Shell.openExternal(url); + else + window.open(url); + }; + + function crossPlatformFilename (basename) { + return basename + // Replace D/M/Y with D-M-Y, and H:M:S with H.M.S. + .replace(/\//g, "-").replace(/:/g, ".") + // Replace all other problematic characters with _. + .replace(/["<>*?|\\]/g, "_"); + } + + yuu.downloadURL = function (url, suggestedName) { + var regex = /^data:[^;+]+;base64,(.*)$/; + var matches = url.match(regex); + suggestedName = crossPlatformFilename(suggestedName); + if (matches && fs) { + var data = matches[1]; + var buffer = new Buffer(data, 'base64'); + var HOME = process.env.HOME + || process.env.HOMEPATH + || process.env.USERPROFILE; + var filename = HOME + "/" + suggestedName; + console.log("Saving to", filename); + fs.writeFileSync(filename, buffer); + } else { + var link = document.createElement('a'); + link.style.display = "none"; + link.href = url; + link.download = suggestedName; + // Firefox (as of 28) won't download from a link not rooted in + // the document; so, root it and then remove it when done. + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; + + yuu.AABB = yT({ + constructor: yf.argcd( + function () { this.constructor(0, 0, 0, 0); }, + function (w, h) { this.constructor(0, 0, w, h); }, + function (x0, y0, x1, y1) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + } + ), + + w: { + get: function () { return this.x1 - this.x0; }, + set: function (w) { this.x1 = this.x0 + w; } + }, + + h: { + get: function () { return this.y1 - this.y0; }, + set: function (h) { this.y1 = this.y0 + h; } + }, + + size: { swizzle: "wh" }, + + contains: yf.argcd( + function (p) { return this.contains(p.x, p.y); }, + function (x, y) { + return x >= this.x0 && x < this.x1 + && y >= this.y0 && y < this.y1; + } + ), + + matchAspectRatio: function (outer) { + var matched = new this.constructor( + this.x0, this.y0, this.x1, this.y1); + var aRatio = matched.w / matched.h; + var bRatio = outer.w / outer.h; + if (aRatio > bRatio) { + // too wide, must be taller + var h = matched.w / bRatio; + var dh = h - matched.h; + matched.y0 -= dh / 2; + matched.y1 += dh / 2; + } else { + // too tall, must be wider + var w = matched.h * bRatio; + var dw = w - matched.w; + matched.x0 -= dw / 2; + matched.x1 += dw / 2; + } + return matched; + }, + + alignedInside: function (outer, alignment) { + var x0, y0; + switch (alignment) { + case "bottomleft": + x0 = outer.x0; + y0 = outer.y0; + break; + case "bottom": + x0 = outer.x0 + (outer.w - this.w) / 2; + y0 = outer.y0; + break; + case "bottomright": + x0 = outer.x0 - this.w; + y0 = outer.y0; + break; + case "left": + x0 = outer.x0; + y0 = outer.x0 + (outer.h - this.h) / 2; + break; + case "center": + x0 = outer.x0 + (outer.w - this.w) / 2; + y0 = outer.x0 + (outer.h - this.h) / 2; + break; + case "right": + x0 = outer.x1 - this.w; + y0 = outer.x0 + (outer.h - this.h) / 2; + break; + case "topleft": + x0 = outer.x0; + y0 = outer.y1 - this.h; + break; + case "top": + x0 = outer.x0 + (outer.w - this.w) / 2; + y0 = outer.y1 - this.h; + break; + case "topright": + x0 = outer.x1 - this.w; + y0 = outer.y1 - this.h; + break; + } + return new this.constructor(x0, y0, x0 + this.w, y0 + this.h); + } + }); + + function splitPathExtension (path) { + var dot = path.lastIndexOf("."); + if (dot <= 0) return [path, ""]; + + var dir = path.lastIndexOf("/"); + if (dot < dir) return [path, ""]; + + return [path.substring(0, dot), path.substring(dot)]; + } + yuu.splitPathExtension = splitPathExtension; + + if (stringLerp) { + yT.defineProperty(String.prototype, "lerp", function (b, p) { + b = b.toString(); + // Never numericLerp - if that's desired force Numbers. + // Be more conservative than stringLerp since this runs + // often and the diff can't be easily hoisted. + return this.length * b.length > 256 + ? stringLerp.fastLerp(this, b, p) + : stringLerp.diffLerp(this, b, p); + }); + } + + yT.defineProperties(Number.prototype, { + lerp: function (b, p) { return this + (b - this) * p; } + }); + + yT.defineProperties(Array.prototype, { + lerp: function (b, p) { + var length = Math.round(this.length.lerp(b.length, p)); + var c = new this.constructor(length); + for (var i = 0; i < length; ++i) { + if (i >= this.length) + c[i] = b[i]; + else if (i >= b.length) + c[i] = this[i]; + else + c[i] = this[i].lerp(b[i], p); + } + return c; + } + }); + + /** Typed array extensions + + https://www.khronos.org/registry/typedarray/specs/1.0/ + BUT: Read on for fun times in browser land~ + + Ideally we could just set these once on ArrayBufferView, but + the typed array specification doesn't require that such a + constructor actually exist. And in Firefox (18), it doesn't. + + More infurating, in Safari (7.0.3) Int8Array etc. are not + functions so this needs to be added to the prototype + directly. This is a violation of the specification which + requires such constructors, and ECMA which requires + constructors be functions, and common decency. + */ + + [ Float32Array, Float64Array, Int8Array, Uint8Array, + Int16Array, Uint16Array, Int32Array, Uint32Array + ].forEach(function (A) { + yT.defineProperties(A.prototype, { + slice: yf.argcd( + /** Like Array's slice, but for typed arrays */ + function () { return new this.constructor(this); }, + function (begin) { + return new this.constructor(this.subarray(begin)); + }, + function (begin, end) { + return new this.constructor(this.subarray(begin, end)); + } + ), + + fill: Array.prototype.fill, + reverse: Array.prototype.reverse, + + lerp: function (b, p) { + if (p === 0) + return this.slice(); + else if (p === 1) + return b.slice(); + var c = new this.constructor(this.length); + for (var i = 0; i < this.length; ++i) + c[i] = this[i] + (b[i] - this[i]) * p; + return c; + } + }); + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" ? (this.yuu = {}) : exports); diff --git a/src/yuu/data/license.txt b/src/yuu/data/license.txt new file mode 100644 index 0000000..6c2b4e7 --- /dev/null +++ b/src/yuu/data/license.txt @@ -0,0 +1,507 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + +-- + +glMatrix - http://glmatrix.net/ + +Copyright 2013 Brandon Jones, Colin MacKenzie IV + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. + +-- + +Hammer.js - http://eightmedia.github.io/hammer.js/ + +Copyright 2011-2014 by Jorik Tangelder (Eight Media) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- + +Fonts + +Copyright (c) 2014, Mozilla Foundation https://mozilla.org/ +Copyright (c) 2014, Telefonica S.A. +with Reserved Font Name Fira Sans. + +Copyright (c) 2014, Mozilla Foundation https://mozilla.org/ +Copyright (c) 2014, Telefonica S.A. +with Reserved Font Name Fira Mono. + +Copyright (c) 2014, Dave Gandy http://fontawesome.io/ +with Reserved Font Name Font Awesome + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +-- + +This program may be distributed along with node-webkit, a wrapper for +packaging web applications for standalone use. If so, the accompanying +`node-webkit credits.html' contains its licensing information. + +This program is not a derivative work of node-webkit but rather "mere +aggregation." You do not need to account for node-webkit's licensing +terms to modify and/or redistribute parts of this program unless you +also modify and/or redistribute node-webkit. diff --git a/src/yuu/data/shaders/color.frag b/src/yuu/data/shaders/color.frag new file mode 100644 index 0000000..3d6f67e --- /dev/null +++ b/src/yuu/data/shaders/color.frag @@ -0,0 +1,12 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +precision mediump float; + +varying vec4 fColor; + +void main(void) { + gl_FragColor = fColor; +} diff --git a/src/yuu/data/shaders/default.frag b/src/yuu/data/shaders/default.frag new file mode 100644 index 0000000..712aa80 --- /dev/null +++ b/src/yuu/data/shaders/default.frag @@ -0,0 +1,15 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +precision mediump float; + +varying vec2 fTexCoord; +varying vec4 fColor; +uniform sampler2D tex; + +void main(void) { + gl_FragColor = vec4(fColor.rgb * fColor.a, fColor.a) + * texture2D(tex, fTexCoord); +} diff --git a/src/yuu/data/shaders/default.vert b/src/yuu/data/shaders/default.vert new file mode 100644 index 0000000..5a21e7d --- /dev/null +++ b/src/yuu/data/shaders/default.vert @@ -0,0 +1,22 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +precision mediump float; + +attribute vec3 position; +attribute vec2 texCoord; +attribute vec4 color; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; +varying vec2 fTexCoord; +varying vec4 fColor; + +void main(void) { + gl_Position = projection * view * model * vec4(position, 1.0); + fTexCoord = texCoord; + fColor = color; +} diff --git a/src/yuu/data/yuu.css b/src/yuu/data/yuu.css new file mode 100644 index 0000000..86b933f --- /dev/null +++ b/src/yuu/data/yuu.css @@ -0,0 +1,369 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +@font-face { + font-family: 'FontAwesome'; + src: url('../../ext/font-awesome.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-UltraLight.woff'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-UltraLightItalic.woff'); + font-weight: 200; + font-style: italic; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Regular.woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Italic.woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Bold.woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-BoldItalic.woff'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: 'Fira Mono'; + src: url('../../ext/FiraMono-Regular.woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Fira Mono'; + src: url('../../ext/FiraMono-Bold.woff'); + font-weight: 700; + font-style: normal; +} + +pre, tt, code, kbd { + font-family: 'Fira Mono', FontAwesome, monospace; +} + +body { + overflow: hidden; + margin: 0; + padding: 0; + font-family: 'Fira Sans', FontAwesome, sans-serif; +} + +#yuu-canvas { + /* Specifying only width/height gives incorrect results on Chrome + 33.0.1750.152 when fullscreen on > 1 devicePixelRatio. The + canvas takes on the correct size, but is centered in a page of + e.g. 2x for 2 DPR so you only see the top-left quadrant. + + Specifying only top/bottom/left/right 0 also breaks, because in the + absence of a CSS size the browser tries to set the client size to + the canvas buffer size, which means it grows/shrinks by the DPR + every resize event. + + Specifying all six attributes makes it work as desired. */ + position: absolute; + width: 100%; + height: 100%; + bottom: 0; + left: 0; + right: 0; + top: 0; +} + +#yuu-canvas:focus { + outline: inherit; +} + +#yuu-licensing { + padding-left: 2em; + padding-right: 2em; + text-align: center; + font-size: 0.7em; +} + +pre#yuu-licensing { + text-align: left; +} + +[data-yuu-command] { + cursor: pointer; +} + +/* Animations */ + +.yuu-from-top-right { + transform: translate(50vw, -110%) !important; + -webkit-transform: translate(50vw, -110%) !important; +} + +.yuu-from-top { + transform: translate(-50%, -110%) !important; + -webkit-transform: translate(-50%, -110%) !important; +} + +.yuu-fade { + opacity: 0 !important; +} + +.yuu-squish { + margin-left: 0 !important; + margin-right: 0 !important; + max-width: 0 !important; + min-width: 0 !important; + overflow: hidden !important; + padding-left: 0em !important; + padding-right: 0em !important; +} + +/* Toasts are short-lived feedback to user actions. */ +.yuu-toast { + pointer-events: none; + background-color: rgba(50, 50, 50, 0.5); + border-radius: 0.2em; + border: solid rgba(255, 255, 255, 0.5) 1px; + color: #eee; + display: table-cell; + float: right; + font-size: 3em; + margin-left: 0.125em; + margin-right: 0.125em; + margin-top: 0.25em; + max-width: 60%; + /* Minimum size is a square: 1.25 + 0.125 * 2 for padding = 1.5em, + same as the line height. */ + min-width: 1.4em; + padding: 0 0.125em; + padding-top: 0.15em; + position: relative; + text-align: center; + transition: all 0.5s; + -webkit-transition: all 0.5s; +} + +/* Overlays are hidden HTML-based scenes that the director can load. + These appear over the game, and are modal. The primary use case is + configuration menus, copyright information, error feedback, + etc. */ + +.yuu-overlay { + background-color: rgba(50, 50, 50, 0.9); + border-radius: 0.2em; + border: solid rgba(255, 255, 255, 0.9) 1px; + color: #eee; + display: none; + width: 60%; + max-width: 600px; + min-width: 300px; + margin-left: auto; + margin-right: auto; + left: 50%; + max-height: 80%; + overflow: auto; + padding: 0 2em 1em 2em; + position: fixed; + transform: translate(-50%, 10vh) scale(1, 1); + -webkit-transform: translate(-50%, 10vh) scale(1, 1); + transition: transform 0.3s, opacity 0.3s; + -webkit-transition: -webkit-transform 0.3s, opacity 0.3s; +} + +/* Overlays are focusable but should not show it - they are always + somewhere in the event tree when visible. */ +.yuu-overlay:focus { + outline: inherit; +} + +.yuu-overlay h1 { + font-size: 1.2em; + font-weight: normal; + text-align: center; +} + +.yuu-overlay h2 { + font-size: 1.1em; + font-weight: normal; +} + +.yuu-overlay hr { + margin-bottom: 1em; + margin-top: 0.5em; +} + +/* For consistency overlays use custom CSS for controls, which + means we need a default focused behavior. */ +.yuu-overlay *:focus { + outline: solid grey 1px; +} + +div[data-yuu-command=dismiss] { + font-size: 1.5em; + width: 1.25em; + height: 1.25em; + text-align: center; + margin-left: -1.3333em; + position: sticky; + top: 0; +} + +div[data-yuu-command=dismiss]:after { + content: "\f00d"; +} + +/* Table layout for options screens. In general, two or three columns, + the leftmost is a simple control, the middle/last is a label, and + the last is a more complicated control like a range or select + dropdown. */ + +.yuu-options { + border-collapse: separate; + border-spacing: 0.25em; +} + +.yuu-options td:first-child { + min-width: 2em; + white-space: nowrap; +} + +.yuu-options td:last-child { + width: 100%; +} + +/* De/re-style checkboxes. This means hiding the actual + checkbox and making it tiny, and instead filling in the + label immediately after it. */ +input[type=checkbox][data-yuu-command] { + max-width: 0; + opacity: 0; +} + +input[type=checkbox][data-yuu-command] + label[for] { + cursor: pointer; + display: inline-block; + text-align: center; + width: 1.3333em; + font-size: 1.25em; +} + +input[type=checkbox][data-yuu-command] + label[for]:before { + display: inline-block; + padding-top: 0.2em; +} + +input[type=checkbox][data-yuu-command] + label[for]:before { + content: "\f096"; +} + +input[type=checkbox][data-yuu-command]:checked + label[for]:before { + content: "\f046"; +} + +input[type=checkbox][data-yuu-command]:focus + label[for] { + outline: solid grey 1px; +} + +/* De/re-style ranges. */ +input[type=range][data-yuu-command] { + -webkit-appearance: none; + background-color: gray; +} + +input[type=range][data-yuu-command]::-moz-range-track { + background: gray; + border: none; + outline: none; +} + +input[type=range][data-yuu-command]::-webkit-slider-thumb { + -webkit-appearance: none; + background-color: #444; + width: 1.5em; + height: 1em; +} + +input[type=range][data-yuu-command]::-moz-range-thumb { + border: none; + background-color: #444; + width: 1.5em; + height: 1em; +} + +/* Special-case icons for the mute checkbox. */ + +input[type=checkbox][data-yuu-command=mute]:checked + label[for]:before { + content: "\f026"; +} + +input[type=checkbox][data-yuu-command=mute] + label[for]:before { + content: "\f028"; +} + +@-moz-keyframes spin { + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(360deg); } +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.yuu-spinner:after { + content: "◔"; + -webkit-animation: spin 1s linear infinite; + -moz-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; + display: inline-block; +} + +dl { + text-align: center; +} + +dt { + margin-top: 1em; + margin-bottom: 0.25em; + font-size: 0.8em; + font-weight: 200; +} + +dd { + margin-left: 0; +} + +body.standalone .browser, body.browser .standalone { + display: none; +} diff --git a/src/yuu/director.js b/src/yuu/director.js new file mode 100644 index 0000000..e1c0b53 --- /dev/null +++ b/src/yuu/director.js @@ -0,0 +1,689 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + // It's vaguely plausible to want a director without any scenes + // (only entity0 and the canvas), which means the renderer is not + // required. + if (!yuu.E) require("./ce"); + if (!yuu.InputState) require("./input"); + if (!yuu.Material) require("./gfx"); + + yuu.Director = yT({ + constructor: function (commandStack, input, tickHz) { + /** Manage and update a set of Scenes + + The director is responsible for calling two functions + regularly on the Scene instances it controls, `tick` and + `render`. `tick` is called at a regular interval (or at + least pretends to be called at one, browser scheduler + permitting), and `render` is called when the browser asks + for a new display frame. + */ + this._scenes = []; + this.entity0 = new yuu.E(); + this._commandStack = commandStack || yuu.commandStack; + this.input = input || new yuu.InputState([yuu.defaultKeybinds]); + this._events = {}; + this._tickCount = 0; + this._timerStart = 0; + this._audioOffset = 0; + this._rafId = null; + this._tickHz = tickHz || 60; + this._afterRender = []; + + this.commands = yuu.extractCommands(this); + this._commandStack.push(this.commands); + this._dogesture = this.__dogesture.bind(this); + this._gesture = null; + this._resized = false; + this._toasts = {}; + this._devices = {}; + }, + + pushScene: function (scene) { + /** Add a Scene onto the director's stack */ + this.insertScene(scene, this._scenes.length); + }, + + popScene: function () { + /** Remove the top scene from the director's stack */ + this.removeScene(yf.last(this._scenes)); + }, + + pushPopScene: function (scene) { + /** Replace the top scene on the stack */ + this.popScene(); + this.pushScene(scene); + }, + + insertScene: function (scene, idx) { + var scenes = this._scenes.slice(); + scenes.splice(idx, 0, scene); + this._scenes = scenes; + this._commandStack.insertBefore( + scene.commands, + this._scenes[idx + 1] && this._scenes[idx + 1].commands); + this.input.insertBefore( + scene.keybinds, + this._scenes[idx + 1] && this._scenes[idx + 1].keybinds); + scene.init(this); + if (scene.inputs.resize) + scene.inputs.resize.call(scene, yuu.canvas); + }, + + insertUnderScene: function (scene, over) { + return this.insertScene(scene, this._scenes.indexOf(over)); + }, + + removeScene: function (scene) { + /** Remove a Scene onto the director's stack */ + this._scenes = yf.without(this._scenes, scene); + scene.done(); + this.input.remove(scene.keybinds); + this._commandStack.remove(scene.commands); + }, + + DOCUMENT_EVENTS: [ "keydown", "keyup", "visibilitychange" ], + + CANVAS_EVENTS: [ "mousemove", "mousedown", "mouseup" ], + + WINDOW_EVENTS: [ "popstate", "resize", "pageshow", + "yuugamepadbuttondown", "yuugamepadbuttonup" ], + + GESTURES: [ + "touch", "release", "hold", "tap", "doubletap", + "dragstart", "drag", "dragend", "dragleft", "dragright", + "dragup", "dragdown", "swipe", "swipeleft", "swiperight", + "swipeup", "swipedown", "pinch", "pinchin", "pinchout" + ], + + _dispatchSceneInput: function (name, args) { + var scenes = this._scenes; + for (var i = scenes.length - 1; i >= 0; --i) { + var scene = scenes[i]; + var handler = scene.inputs[name]; + if (handler && handler.apply(scene, args)) + return true; + + // FIXME: This may be a heavy ad hoc solution for the + // multiple input layer problems in pwl6. Something + // like this isn't required or allowed for e.g. + // individual keys, why not? + // + // REALLY FIXME: This doesn't even work correctly for + // joystick events, a) because they're prefixed and b) + // because they are in WINDOW_EVENTS so you have to + // manually enumerate them or you also ignore e.g. + // resize. + else if (scene.inputs.consume + && yf.contains(scene.inputs.consume, name)) + return false; + } + return false; + }, + + // Aside from the performance considerations, deferring + // resizing by multiple frames fixes mis-sizing during startup + // and fullscreen transition in node-webkit on Windows. (And + // probably similar bugs in other configurations.) + _doresize: yf.debounce(function () { + this._resized = true; + }, 500), + + _dovisibilitychange: function (event) { + if (event.target.hidden) + this._stopRender(); + else + this._startRender(); + }, + + _dopageshow: function (event) { + if (!history.state) + history.pushState("yuu director", ""); + this._stopRender(); + if (!document.hidden) + this._startRender(); + }, + + _dopopstate: function (event) { + var cmds = []; + if (this._dispatchSceneInput("back", []) + || (cmds = this.input.change("back"))) { + history.pushState("yuu director", ""); + yf.each.call(this, this.execute, cmds); + yuu.stopPropagation(event, true); + } else { + history.back(); + } + }, + + __dogesture: function (event) { + this._updateCaps(event.gesture.srcEvent.type.toLowerCase(), true); + var type = event.type.toLowerCase(); + var p0 = yuu.deviceFromCanvas(event.gesture.startEvent.center); + var p1 = yuu.deviceFromCanvas(event.gesture.center); + if (this._dispatchSceneInput(type, [p0, p1])) + yuu.stopPropagation(event, true); + }, + + // TODO: This munges events, but also, InputState's mousemove + // etc. munge events, in a slightly different but still + // related and fragile way. + // + // Additionally, things run in a Scene handler won't + // affect the InputState's internal state - good for + // avoiding bind execution, bad for consistency. Even if + // a scene handles e.g. "keydown a", input.pressed.a + // should be true. + // + // This is compounded by the lack of actual use cases for any + // of the non-gesture events other than "back" and + // "mousemove". + + _ARGS_FOR: { + keydown: function (event) { + return [yuu.keyEventName(event), {}]; + }, + keyup: function (event) { + return [yuu.keyEventName(event), {}]; + }, + mousemove: function (event) { + return [yuu.deviceFromCanvas(event)]; + }, + mouseup: function (event) { + return [event.button, yuu.deviceFromCanvas(event)]; + }, + mousedown: function (event) { + return [event.button, yuu.deviceFromCanvas(event)]; + }, + gamepadbuttondown: function (event) { + return [event.detail.gamepad, + event.detail.button]; + }, + gamepadbuttonup: function (event) { + return [event.detail.gamepad, event.detail.button]; + }, + }, + + _updateCaps: function (type, definite) { + if (type.startsWith("mouse")) { + if (this._devices.mouse === undefined || definite) + this._devices.mouse = Date.now(); + this._devices.touch = this._devices.touch || false; + } else if (type.startsWith("touch")) { + this._devices.mouse = this._devices.mouse || false; + this._devices.touch = Date.now(); + this._devices.keyboard = this._devices.keyboard || false; + } else if (type.startsWith("key")) { + this._devices.keyboard = Date.now(); + } else if (type.startsWith("gamepad")) { + this._devices.gamepad = Date.now(); + this._devices.touch = this._devices.touch || false; + } + }, + + preferredDevice: function (options) { + options = options || ["keyboard", "touch", "mouse", "gamepad"]; + var devices = this._devices; + var best = yf.foldl(function (best, option) { + var dbest = devices[best]; + var doption = devices[option]; + return dbest === undefined && doption ? option + : doption > dbest ? option : best; + }, options); + for (var i = 0; devices[best] === false && i < options.length; ++i) + if (devices[options[i]] !== false) + best = options[i]; + return best; + }, + + _doevent: function (event) { + var type = event.type.toLowerCase(); + if (type.startsWith("yuu")) + type = type.slice(3); + var args = this._ARGS_FOR[type](event); + var cmds; + this._updateCaps(type, false); + if (this._dispatchSceneInput(type, args)) + yuu.stopPropagation(event, true); + else if ((cmds = this.input[type].apply(this.input, args))) { + var ctx = yf.last(args); + yf.each.call(this, this.execute, cmds, yf.repeat(ctx, cmds.length)); + yuu.stopPropagation(event, true); + } + }, + + _addListener: function (target, name, handler) { + handler = (handler || this["_do" + name] || this._doevent).bind(this); + this._events[name] = { target: target, handler: handler }; + target.addEventListener(name, handler); + }, + + _removeListener: function (name) { + this._events[name].target.removeEventListener( + name, this._events[name].handler); + delete this._events[name]; + }, + + tickHz: { + get: function () { return this._tickHz; }, + set: function (hz) { + this._tickHz = hz; + this._tickCount = 0; + this._timerStart = 0; + } + }, + + currentTime: { get: function () { + return this._timerStart + 1000 * this._tickCount / this._tickHz; + } }, + + currentAudioTime: { get: function () { + /** Audio time of the current tick. + */ + return (this.currentTime + this._audioOffset) / 1000; + } }, + + _startRender: function () { + if (this._rafId !== null) + return; + this._tickCount = 0; + this._timerStart = 0; + // GNU/Linux with node-webkit sizes things incorrectly on + // startup, so force a recalculating as soon as the render + // loop runs. + this._resized = true; + var director = this; + this._rafId = window.requestAnimationFrame(function _ (t) { + if (!director._timerStart) { + director._timerStart = t; + director._audioOffset = yuu.audio + ? yuu.audio.currentTime * 1000 - t + : 0; + } + director._rafId = window.requestAnimationFrame(_); + director.render(t); + }); + }, + + _stopRender: function () { + if (this._rafId !== null) + window.cancelAnimationFrame(this._rafId); + this._rafId = null; + }, + + start: function () { + /** Begin ticking and rendering scenes */ + yf.each(this._addListener.bind(this, window), + this.WINDOW_EVENTS); + yf.each(this._addListener.bind(this, document), + this.DOCUMENT_EVENTS); + yf.each(this._addListener.bind(this, yuu.canvas), + this.CANVAS_EVENTS); + + this._gesture = typeof Hammer !== "undefined" + ? new Hammer(yuu.canvas, { "tap_always": false, + "hold_timeout": 300 }) + : { on: function () {}, off: function () {} }; + this._gesture.on(this.GESTURES.join(" "), this._dogesture); + + // Treat the back button as another kind of input event. Keep + // a token state on the stack to catch the event, and if no + // scene handles it, just go back one more. + // + // Because of browser session restore, state might already be + // on the stack. Throw it out if so. + if (!history.state) + history.pushState("yuu director", ""); + else + history.replaceState("yuu director", ""); + this._startRender(); + }, + + stop: function () { + /** Stop ticking and rendering, clear all scenes */ + this._stopRender(); + yf.eachr(function (scene) { scene.done(); }, this._scenes); + this._scenes = []; + yf.each.call(this, this._removeListener, Object.keys(this._events)); + this._gesture.off(this.GESTURES.join(" "), this._dogesture); + this._gesture = null; + }, + + message: function () { + /** Send a message to all entities/scenes, bottom to top */ + this.entity0.message.apply(this.entity0, arguments); + var scenes = this._scenes; + for (var i = 0; i < scenes.length; ++i) + scenes[i].message.apply(scenes[i], arguments); + }, + + _takeScreenshot: function () { + var date = (new Date()).toLocaleString(); + try { + yuu.downloadURL( + yuu.canvas.toDataURL("image/png"), + document.title + " (" + date + ").png"); + this.toast("\uf030", 0.5, "screenshot"); + } catch (exc) { + var dialog = yuu.showError(exc); + if (dialog) + this.showOverlay(dialog.id); + } + }, + + render: function (t) { + /** Tick and render all scenes, bottom to top */ + var i; + + if (this._resized) { + this._dispatchSceneInput("resize", [yuu.canvas]); + this._resized = false; + } + + t = t - this._timerStart; + var oneTick = 1000.0 / this._tickHz; + while (oneTick * this._tickCount < t) + this.message("tick", oneTick * this._tickCount++, oneTick); + this.message("tock", (t % oneTick) / oneTick); + + yuu.gl.clear(yuu.gl.COLOR_BUFFER_BIT); + var scenes = this._scenes; + var cursor = "default"; + for (i = 0; i < scenes.length; ++i) { + scenes[i].render(); + cursor = scenes[i].cursor || cursor; + } + + if (cursor !== yuu.canvas.style.cursor) + yuu.canvas.style.cursor = cursor; + + for (i = 0; i < this._afterRender.length; ++i) + this._afterRender[i](); + this._afterRender.length = 0; + }, + + toast: yuu.cmd(function (markup, duration, id) { + var toasts = this._toasts; + id = "yuu-toast-" + id; + var toast = id ? document.querySelector("#" + id) : null; + duration = duration || 4; + + if (!toast) { + toast = document.createElement("div"); + toast.id = id; + toast.className = "yuu-toast yuu-fade"; + document.body.appendChild(toast); + } + if (toasts[id]) { + clearTimeout(toasts[id]); + delete toasts[id]; + } + toast.innerHTML = markup; + yuu.afterAnimationFrame(function () { + toast.className = "yuu-toast"; + }); + + var to = setTimeout(function () { + toast.className = "yuu-toast yuu-fade"; + toast.addEventListener("transitionend", function fade () { + toast.removeEventListener("transitionend", fade); + // Stop if the toast was revived between the + // timeout event and transition end, i.e. while it + // was fading out. + if (id && toasts[id] !== to) + return; + toast.className += " yuu-squish"; + toast.addEventListener("transitionend", function squish () { + toast.removeEventListener("transitionend", squish); + if (id && toasts[id] === to) { + delete toasts[id]; + toast.parentNode.removeChild(toast); + } + }); + }); + }, duration * 1000); + if (id) + toasts[id] = to; + }, " ", "show a toast message"), + + showOverlay: yuu.cmd(function (id, animation, dismissKeys) { + var overlay = new yuu.Overlay( + document.getElementById(id), animation, dismissKeys); + this.pushScene(overlay); + }, " ", "show an HTML overlay"), + + screenshot: yuu.cmd(function () { + this._afterRender.push(this._takeScreenshot.bind(this)); + }, "take a screenshot"), + + fullscreen: yuu.cmd(function (v) { + if (arguments.length > 0) { + yuu.fullscreen = !!v; + // Most browser/OS combinations will drop key events + // during the "transition to fullscreen" animation. + // This means the key to enter fullscreen is recorded + // as "stuck down" inside the input code, and pressing + // it again won't trigger exiting fullscreen, just + // clear the stuck bit - you would have to press it + // *again* to actually transition out of fullscreen. + // + // Obviously this is not good, and the chance of the + // player actually trying to do something meaningful + // during fullscreen transition is unlikely, so just + // blow away the internal state and act like + // everything the player does is new. + this.input.reset(); + } + return yuu.fullscreen; + }, "", "enable/disable fullscreen"), + + execute: { proxy: "_commandStack.execute" }, + }); + + yuu.Scene = yT({ + constructor: function () { + /** A collection of entities, a layer, keybinds, and commands + + The single argument is as function that will be scalled + during construction with `this` as the newly-created + scene. + + */ + this.entity0 = new yuu.E(); + this.layer0 = new yuu.Layer(); + this.keybinds = new yuu.KeyBindSet(this.KEYBINDS); + this.commands = yuu.extractCommands(this); + }, + + addEntity: { proxy: "entity0.addChild" }, + removeEntity: { proxy: "entity0.removeChild" }, + addEntities: { proxy: "entity0.addChildren" }, + removeEntities: { proxy: "entity0.removeChildren" }, + message: { proxy: "entity0.message" }, + + init: function (director) { + /** Called when the director starts this scene */ + }, + + done: function () { + /** Called when the director stops this scene */ + }, + + render: function () { + /** Queue renderables from the entities and render each layer */ + this.message("queueRenderables", this.layer0.rdros); + this.layer0.render(); + this.layer0.clear(); + }, + + inputs: {}, + KEYBINDS: {} + }); + + yuu.Overlay = yT(yuu.Scene, { + constructor: function (element, animation, dismissKeys) { + yuu.Scene.call(this); + this.dismissKeys = dismissKeys + || (element.getAttribute("data-yuu-dismiss-key") || "").split(" "); + this.animation = animation + || element.getAttribute("data-yuu-animation") + || "yuu-from-top"; + this.element = element; + this.className = element.className; + this._keydown = function (event) { + var name = yuu.keyEventName(event); + if (this.inputs.keydown.call(this, name)) + yuu.stopPropagation(event); + }.bind(this); + }, + + inputs: { + back: function () { this.dismiss(); return true; }, + keydown: function (key) { + if (yf.contains(this.dismissKeys, key)) + this.dismiss(); + return true; + }, + touch: function () { this.dismiss(); return true; }, + mousedown: function () { this.dismiss(); return true; }, + }, + + init: function (director) { + var element = this.element; + var className = this.className; + var elements = element.querySelectorAll("[data-yuu-command]"); + + yf.each(function (element) { + var command = element.getAttribute("data-yuu-command"); + switch (element.tagName.toLowerCase()) { + case "input": + switch (element.type.toLowerCase()) { + case "range": + element.value = director.execute(command); + break; + case "checkbox": + var res = !!director.execute(command); + element.checked = res; + break; + } + break; + } + }, elements); + + this._director = director; + + element.className = className + " " + this.animation; + element.style.display = "block"; + element.tabIndex = 0; + element.focus(); + element.addEventListener("keydown", this._keydown); + + yuu.afterAnimationFrame(function () { + element.className = className; + }); + }, + + dismiss: yuu.cmd(function () { + var element = this.element; + var className = this.className; + var director = this._director; + var scene = this; + element.className = className + " " + this.animation; + element.addEventListener("transitionend", function _ () { + element.removeEventListener("transitionend", _); + director.removeScene(scene); + }); + }, "", "dismiss this overlay"), + + done: function () { + this.element.style.display = "none"; + this.element.tabIndex = -1; + this.element.className = this.className; + this.element.removeEventListener("keydown", this._keydown); + this._director = null; + yuu.canvas.focus(); + }, + + KEYBINDS: { + "escape": "dismiss" + } + }); + + yuu.registerInitHook(function () { + var elements = document.querySelectorAll("[data-yuu-command]"); + + function handleElement (event) { + /*jshint validthis:true */ + /* `this` comes from being a DOM element event handler. */ + var command = this.getAttribute("data-yuu-command"); + switch (this.tagName.toLowerCase()) { + case "input": + switch (this.type.toLowerCase()) { + case "range": + command += " " + this.value; + break; + case "checkbox": + command += " " + (this.checked ? "1" : "0"); + break; + } + break; + } + yuu.director.execute(command); + yuu.stopPropagation(event); + } + + yf.each(function (element) { + switch (element.tagName.toLowerCase()) { + case "input": + switch (element.type.toLowerCase()) { + case "range": + element.oninput = handleElement; + element.onchange = handleElement; + break; + case "checkbox": + element.onchange = handleElement; + break; + } + break; + case "div": + case "span": + element.onclick = handleElement; + element.onkeydown = function (event) { + var name = yuu.keyEventName(event); + if (name === "space" || name === "return") + handleElement.call(this, event); + }; + break; + default: + element.onclick = handleElement; + break; + } + }, elements); + + yuu.defaultKeybinds.bind("control+`", "showDevTools"); + yuu.defaultKeybinds.bind("f11", "++fullscreen"); + yuu.defaultKeybinds.bind("f12", "screenshot"); + yuu.defaultKeybinds.bind( + "control+s", "++mute && toast \uf026 1 mute || toast \uf028 1 mute"); + + var director = yuu.director = new yuu.Director(); + /** The standard director */ + + yuu.registerInitHook(function () { + return yuu.ready(director._scenes); + }); + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/gfx.js b/src/yuu/gfx.js new file mode 100644 index 0000000..2c3088e --- /dev/null +++ b/src/yuu/gfx.js @@ -0,0 +1,665 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + var gl; + var canvas; + + var dpr = yuu.DPR = this.devicePixelRatio || 1; + + yT.defineProperty(Int8Array.prototype, "GL_TYPE", 0x1400); + yT.defineProperty(Uint8Array.prototype, "GL_TYPE", 0x1401); + yT.defineProperty(Int16Array.prototype, "GL_TYPE", 0x1402); + yT.defineProperty(Uint16Array.prototype, "GL_TYPE", 0x1403); + yT.defineProperty(Int32Array.prototype, "GL_TYPE", 0x1404); + yT.defineProperty(Uint32Array.prototype, "GL_TYPE", 0x1405); + yT.defineProperty(Float32Array.prototype, "GL_TYPE", 0x1406); + /** Patch the WebGL type onto arrays for data-driven access later + + Values from https://www.khronos.org/registry/webgl/specs/1.0/. + + See also notes in pre on Safari's typed array problems. + */ + + yuu.uniform = function (location, value) { + /** Set a uniform in the active program + + The type of the uniform is automatically determined from + the value: + + * Typed integer arrays of length 1-4 call uniform[1-4]iv + * Other sequences of length 1-4 call uniform[1-4]fv + * Sequences of length 9 or 16 call uniformMatrix[3-4]fv + * Non-sequences call uniform1fv (even if the parameter + is a valid integer) + * Sequences of other lengths throw a TypeError + + It is not possible to call uniformMatrix2fv via this + function. + */ + switch (value.constructor) { + case Int8Array: + case Uint8Array: + case Int16Array: + case Uint16Array: + case Int32Array: + case Uint32Array: + switch (value.length) { + case 1: return gl.uniform1iv(location, value); + case 2: return gl.uniform2iv(location, value); + case 3: return gl.uniform3iv(location, value); + case 4: return gl.uniform4iv(location, value); + default: throw new TypeError("unexpected array length"); + } + break; + default: + switch (value.length) { + case 1: return gl.uniform1fv(location, value); + case 2: return gl.uniform2fv(location, value); + case 3: return gl.uniform3fv(location, value); + case 4: return gl.uniform4fv(location, value); + case 9: return gl.uniformMatrix3fv(location, false, value); + case 16: return gl.uniformMatrix4fv(location, false, value); + case undefined: return gl.uniform1f(location, value); + default: throw new TypeError("unexpected array length"); + } + } + }; + + function isShaderSource (src) { + return src.indexOf("\n") >= 0 || yf.last(src.trim()) === ";"; + } + + var FRAGMENT_SHADER = 0x8B30; + var VERTEX_SHADER = 0x8B31; + var EXTS = {}; + EXTS[FRAGMENT_SHADER] = "frag"; + EXTS[VERTEX_SHADER] = "vert"; + + function compile (type, srcs) { + function getSource (src) { + return isShaderSource(src) + ? Promise.resolve(src) + : yuu.GET(yuu.resourcePath(src, "shaders", EXTS[type])); + } + return Promise.all(yf.map(getSource, srcs)) + .then(function (srcs) { + var src = srcs.join("\n"); + var shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + var log = gl.getShaderInfoLog(shader); + throw new Error( + "Shader compile error:\n\n" + src + "\n\n" + log); + } + return shader; + }); + } + + yuu.ShaderProgram = yT({ + constructor: function (vs, fs) { + /** A linked program of vertex and fragment shaders + + vs and fs are arrays of vertex and fragment shader source + code or URLs. + */ + fs = fs || ["yuu/@default"]; + vs = vs || ["yuu/@default"]; + var id = this.id = gl.createProgram(); + var attribs = this.attribs = {}; + var uniforms = this.uniforms = {}; + this.ready = Promise.all([compile(VERTEX_SHADER, vs), + compile(FRAGMENT_SHADER, fs)]) + .then(function (shaders) { + yf.each(gl.attachShader.bind(gl, id), shaders); + gl.linkProgram(id); + if (!gl.getProgramParameter(id, gl.LINK_STATUS)) + throw new Error("Shader link error: " + + gl.getProgramInfoLog(id)); + return id; + }).catch(function (exc) { + yuu.showError(exc); + this.id = yuu.ShaderProgram.DEFAULT.id; + this.attribs = yuu.ShaderProgram.DEFAULT.attribs; + this.uniforms = yuu.ShaderProgram.DEFAULT.uniforms; + throw exc; + }.bind(this)).then(function (id) { + this.id = id; + yf.irange(function (i) { + var name = gl.getActiveAttrib(id, i).name; + attribs[name] = gl.getAttribLocation(id, name); + }, gl.getProgramParameter(id, gl.ACTIVE_ATTRIBUTES)); + yf.irange(function (i) { + var name = gl.getActiveUniform(id, i).name; + uniforms[name] = gl.getUniformLocation(id, name); + }, gl.getProgramParameter(id, gl.ACTIVE_UNIFORMS)); + return this; + }.bind(this)); + }, + + setUniforms: function () { + /** Set the values of program uniforms + + The arguments are any number of objects mapping + uniform names to values (floats, vec3s, etc.). + */ + for (var i = 0; i < arguments.length; ++i) + for (var name in arguments[i]) + yuu.uniform(this.uniforms[name], arguments[i][name]); + }, + + setAttribPointers: function (buffer) { + /** Bind the contents of a vertex buffer to attributes + + `buffer` is (or is like) a yuu.VertexBuffer instance. + */ + for (var name in this.attribs) + gl.vertexAttribPointer( + this.attribs[name], + buffer.spec.attribs[name].elements, + buffer.spec.attribs[name].View.prototype.GL_TYPE, + false, 0, buffer.arrays[name].byteOffset); + } + }); + + // This function is easier to read than a giant lookup table + // ({ textureWrapS: "TEXTURE_WRAP_S", ... x100 }) but slower. + function glEnum (gl, name) { + return gl[name.replace(/([A-Z]+)/g, "_$1").toUpperCase()]; + } + + function glScopedEnum (scope, gl, name) { + var value = glEnum(gl, scope + "_" + name); + if (value === undefined) + value = glEnum(gl, name); + return value; + } + + var glTextureEnum = glScopedEnum.bind(null, "texture"); + + yuu.Texture = yuu.Caching(yT({ + constructor: function (path, overrideOptions) { + /** A 2D texture + + The texture is set to a 1x1 white texture until it is + loaded (or if loading fails). + */ + var options = {}; + yf.ipairs(function (k, v) { + options[glTextureEnum(gl, k)] = glEnum(gl, v); + }, TEXTURE_DEFAULTS); + yf.ipairs(function (k, v) { + options[glTextureEnum(gl, k)] = glEnum(gl, v); + }, overrideOptions || {}); + + if (!path) { + var data = new Uint8Array([255, 255, 255, 255]); + this.id = gl.createTexture(); + this.width = this.height = 1; + this.src = "default / fallback 1x1 white texture"; + gl.bindTexture(gl.TEXTURE_2D, this.id); + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, + gl.RGBA, gl.UNSIGNED_BYTE, data); + gl.texParameteri( + gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri( + gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, null); + this.ready = Promise.resolve(this); + return; + } + + path = yuu.resourcePath(path, "images", "png"); + this.id = yuu.Texture.DEFAULT.id; + this.src = path; + this.width = yuu.Texture.DEFAULT.width; + this.height = yuu.Texture.DEFAULT.height; + + this.ready = yuu.Image(path).then(function (img) { + var id = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, id); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + for (var opt in options) + gl.texParameteri(gl.TEXTURE_2D, opt, options[opt]); + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, + gl.RGBA, gl.UNSIGNED_BYTE, img); + gl.bindTexture(gl.TEXTURE_2D, null); + this.id = id; + this.width = img.width; + this.height = img.height; + this.src = img.src; + return this; + }.bind(this)).catch(function (e) { + this.src = "Error loading " + path + ": " + e; + yuu.log("errors", this.src); + gl.bindTexture(gl.TEXTURE_2D, null); + throw e; + }.bind(this)); + } + })); + + var TEXTURE_DEFAULTS = yuu.Texture.DEFAULTS = { + magFilter: "linear", + minFilter: "linear", + wrapS: "clampToEdge", + wrapT: "clampToEdge" + }; + + yuu.Material = yuu.Caching(yT({ + constructor: function (texture, program, uniforms) { + /** A material is a combination of a texture and shader program */ + if (yf.isString(texture)) + texture = new yuu.Texture(texture); + this.texture = texture || yuu.Texture.DEFAULT; + this.program = program || yuu.ShaderProgram.DEFAULT; + this.ready = yuu.ready([this.texture, this.program], this); + this.uniforms = uniforms || {}; + }, + + enable: function (uniforms) { + /** Enable this material and its default parameters */ + gl.bindTexture(gl.TEXTURE_2D, this.texture.id); + gl.useProgram(this.program.id); + for (var attrib in this.program.attribs) + gl.enableVertexAttribArray(this.program.attribs[attrib]); + this.program.setUniforms(this.uniforms, uniforms); + }, + + disable: function () { + /** Disable this material */ + gl.bindTexture(gl.TEXTURE_2D, null); + gl.useProgram(null); + for (var attrib in this.program.attribs) + gl.disableVertexAttribArray(this.program.attribs[attrib]); + } + })); + + yuu.VertexAttribSpec = function (spec) { + /** Ordering and types for vertex buffer layout + + Interleaved vertices (e.g. VTCVTCVTC) are not currently + supported, as ArrayBufferViews are not able to manage a buffer + with this kind of layout. + */ + var byteOffset = 0; + this.attribs = {}; + spec.forEach(function (a) { + var name = a.name; + var elements = a.elements; + var View = a.View || Float32Array; + this.attribs[name] = { elements: elements, + byteOffset: byteOffset, + View: View }; + byteOffset += elements * View.BYTES_PER_ELEMENT; + }, this); + this.bytesPerVertex = byteOffset; + }; + + yuu.V3T2C4_F = new yuu.VertexAttribSpec([ + /** vec3 position; vec2 texCoord; vec4 color; */ + { name: "position", elements: 3 }, + { name: "texCoord", elements: 2 }, + { name: "color", elements: 4 } + ]); + + yuu.IndexBuffer = yT({ + constructor: function (maxIndex, length) { + this._capacity = -1; + this._maxIndex = maxIndex; + this.buffer = null; + this.type = null; + this.length = length; + this._glBuffer = gl.createBuffer(); + this.dirty = true; + }, + + bindBuffer: function () { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._glBuffer); + if (this.dirty) { + this.dirty = false; + gl.bufferData( + gl.ELEMENT_ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW); + } + }, + + GL_TYPE: { alias: "buffer.GL_TYPE" }, + + maxIndex: { + get: function () { return this._maxIndex; }, + set: function (maxIndex) { + var Array = yuu.IndexBuffer.Array(maxIndex); + if (maxIndex > this._maxIndex + && Array !== this.buffer.constructor) { + var buffer = new Array(this._capacity); + if (this.buffer) + buffer.set(this.buffer); + this.buffer = buffer; + this.dirty = true; + } + this._maxIndex = maxIndex; + } + }, + + length: { + get: function () { return this._length; }, + set: function (count) { + if (count > this._capacity) { + var Array = yuu.IndexBuffer.Array(this._maxIndex); + var buffer = new Array(count); + if (this.buffer) + buffer.set(this.buffer); + this.buffer = buffer; + this._capacity = count; + this.dirty = true; + } + this._length = count; + } + } + }); + + yuu.IndexBuffer.Array = function (maxIndex) { + if (maxIndex < 0 || maxIndex >= (256 * 256 * 256 * 256)) + throw new Error("invalid maxIndex index: " + maxIndex); + else if (maxIndex < (1 << 8)) + return Uint8Array; + else if (maxIndex < (1 << 16)) + return Uint16Array; + else + return Uint32Array; + }; + + yuu.VertexBuffer = yT({ + constructor: function (spec, vertexCount) { + /** A buffer with a specified vertex format and vertex count + + The individual vertex attribute array views from the + attribute specification are available via the .arrays + property, e.g. v.arrays.position. The underlying + buffer is available as v.buffer. + + The vertex count may be changed after creation and the + buffer size and views will be adjusted. If you've + grown the buffer, you will need to refill all its + data. Shrinking it will truncate it. + */ + this.spec = spec; + this._vertexCapacity = -1; + this.buffer = null; + this.arrays = {}; + this.vertexCount = vertexCount; + this._glBuffer = gl.createBuffer(); + this.dirty = true; + }, + + bindBuffer: function () { + gl.bindBuffer(gl.ARRAY_BUFFER, this._glBuffer); + if (this.dirty) { + this.dirty = false; + gl.bufferData(gl.ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW); + } + }, + + subdata: function (begin, length) { + return new yuu.VertexBuffer.SubData(this, begin, begin + length); + }, + + vertexCount: { + get: function () { return this._vertexCount; }, + set: function (count) { + if (count > this._vertexCapacity) { + var buffer = new ArrayBuffer( + this.spec.bytesPerVertex * count); + var arrays = {}; + yf.ipairs.call(this, function (name, attrib) { + arrays[name] = new attrib.View( + buffer, attrib.byteOffset * count, + attrib.elements * count); + if (this.arrays[name]) + arrays[name].set(this.arrays[name]); + }, this.spec.attribs); + this.buffer = buffer; + this.arrays = arrays; + this._vertexCapacity = count; + this.dirty = true; + } + this._vertexCount = count; + } + } + }); + + yuu.VertexBuffer.SubData = yT({ + constructor: function (parent, begin, end) { + var arrays = this.arrays = {}; + this._parent = parent; + this.spec = parent.spec; + this.buffer = parent.buffer; + yT.defineProperty(this, "vertexCount", end - begin); + for (var attrib in parent.arrays) { + var s = parent.spec.attribs[attrib].elements; + arrays[attrib] = parent.arrays[attrib].subarray( + begin * s, end * s); + } + }, + + dirty: { alias: "_parent.dirty" } + }); + + var rgbToHsl = yuu.rgbToHsl = yf.argcd( + /** Convert RBG [0, 1] to HSL [0, 1]. */ + function (rgb) { return rgbToHsl.apply(null, rgb); }, + function (r, g, b, a) { + var hsl = rgbToHsl(r, g, b); + hsl[3] = a; + return hsl; + }, + function (r, g, b) { + var max = Math.max(r, g, b); + var min = Math.min(r, g, b); + var h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [h, s, l]; + } + ); + + var hslToRgb = yuu.hslToRgb = yf.argcd( + /** Convert HSL [0, 1] to RGB [0, 1]. */ + function (hsl) { return hslToRgb.apply(null, hsl); }, + function (h, s, l, a) { + var rgb = hslToRgb(h, s, l); + rgb[3] = a; + return rgb; + }, + function (h, s, l) { + var r, g, b; + + function hToC (p, q, t) { + if (t < 0) + t += 1; + if (t > 1) + t -= 1; + if (t < 1 / 6) + return p + (q - p) * 6 * t; + else if (t < 1 / 2) + return q; + else if (t < 2 / 3) + return p + (q - p) * (2/3 - t) * 6; + else + return p; + } + + if (s === 0) { + r = g = b = l; + } else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hToC(p, q, h + 1 / 3); + g = hToC(p, q, h); + b = hToC(p, q, h - 1 / 3); + } + + return [r, g, b]; + } + ); + + var deviceFromCanvas = yuu.deviceFromCanvas = yf.argcd( + /** Convert a point from client to normalized device space + + Normalized device space ranges from [-1, -1] at the + bottom-left of the viewport to [1, 1] at the top-right. + (This is the definition of the space, _not_ bounds on the + return value, as events can happen outside the viewport or + even outside the canvas.) + */ + function (p) { + return deviceFromCanvas(p.x || p.pageX || p[0] || 0, + p.y || p.pageY || p[1] || 0); + }, + function (x, y) { + x -= canvas.offsetLeft; + y -= canvas.offsetTop; + x /= canvas.clientWidth; + y /= canvas.clientHeight; + // xy is now in [0, 1] page space. + + x *= canvas.width; + y *= canvas.height; + // xy now in canvas buffer space. + + var vp = gl.getParameter(gl.VIEWPORT); + var hvpw = vp[2] / 2; + var hvph = vp[3] / 2; + x = (x - vp[0] - hvpw) / hvpw; + y = (y - vp[1] - hvph) / -hvph; + // xy now in normalized device space. + + return { + x: x, 0: x, + y: y, 1: y, + inside: Math.abs(x) <= 1 && Math.abs(y) <= 1 + }; + } + ); + + yuu.viewport = new yuu.AABB(); + + function onresize () { + var resize = canvas.getAttribute("data-yuu-resize") !== null; + var width = +canvas.getAttribute("data-yuu-width"); + var height = +canvas.getAttribute("data-yuu-height"); + + if (resize) { + canvas.width = canvas.clientWidth * dpr; + canvas.height = canvas.clientHeight * dpr; + } + + var vw = canvas.width; + var vh = canvas.height; + if (width && height) { + var aspectRatio = width / height; + if (vw / vh > aspectRatio) + vw = vh * aspectRatio; + else + vh = vw / aspectRatio; + } + var vx = (canvas.width - vw) / 2; + var vy = (canvas.height - vh) / 2; + gl.viewport(vx, vy, vw, vh); + yuu.viewport = new yuu.AABB(vx, vy, vx + vw / dpr, vy + vh / dpr); + } + + yuu.afterAnimationFrame = function (f) { + /* DOM class modifications intended to trigger transitions + must be delayed for at least one frame after the element is + created, i.e. after it has gone through at least one full + repaint. + */ + window.requestAnimationFrame(function () { + setTimeout(f, 0); + }); + }; + + yuu.registerInitHook(function (options) { + var bgColor = options.backgroundColor || [0.0, 0.0, 0.0, 0.0]; + + canvas = this.canvas = document.getElementById("yuu-canvas"); + var glOptions = { + alpha: options.hasOwnProperty("alpha") + ? options.alpha : bgColor[3] !== 1.0, + antialias: options.hasOwnProperty("antialias") + ? options.antialias : true + }; + if (!window.HTMLCanvasElement) + throw new Error(" isn't supported."); + gl = this.gl = canvas.getContext("webgl", glOptions) + || canvas.getContext("experimental-webgl", glOptions); + if (!gl) + throw new Error("WebGL isn't supported."); + + canvas.focus(); + + window.addEventListener('resize', onresize); + onresize(); + + this.ShaderProgram.DEFAULT = new this.ShaderProgram(); + this.Texture.DEFAULT = new this.Texture(); + this.Material.DEFAULT = new this.Material(); + + gl.clearColor.apply(gl, bgColor); + gl.disable(gl.DEPTH_TEST); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.enable(gl.BLEND); + gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); + gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE); + }); + + var gui = yuu.require("nw.gui"); + yT.defineProperty(yuu, "fullscreen", { + get: function () { + return gui + ? gui.Window.get().isFullscreen + : !!(document.fullscreenElement + || document.mozFullScreenElement); + }, + set: function (v) { + if (gui) + gui.Window.get().isFullscreen = !!v; + else if (v) + document.body.requestFullscreen(); + else + document.exitFullscreen(); + } + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/input.js b/src/yuu/input.js new file mode 100644 index 0000000..ba58faf --- /dev/null +++ b/src/yuu/input.js @@ -0,0 +1,268 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + yuu.KEY_NAMES = { + 32: "space", + 13: "return", + 16: "shift", + 18: "alt", + 17: "control", + + 37: "left", + 38: "up", + 39: "right", + 40: "down", + 9: "tab", + 27: "escape", + 8: "backspace", + 191: "slash", + 192: "`", + }; + + // Fill in the key name tables. + for (var i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); ++i) + yuu.KEY_NAMES[i] = String.fromCharCode(i).toLowerCase(); + for (i = "0".charCodeAt(0); i <= "9".charCodeAt(0); ++i) + yuu.KEY_NAMES[i] = String.fromCharCode(i); + for (i = 1; i <= 12; ++i) + yuu.KEY_NAMES[111 + i] = "f" + i; + + function splitKeys (keystring) { + return keystring.toLowerCase().split("+").sort(); + } + + var KeyBind = yT({ + constructor: function (keystring, command) { + /** An individual key to command binding. + + The key string is e.g. "a", "control+f", "left+alt+z". + "f+control" is equivalent to "control+f", and binding one + in a set will override the other. */ + this.keystring = keystring; + this.command = command; + this.keys = splitKeys(keystring); + }, + + uses: function (name) { + /** True if the given key name is relevant to this binding. */ + return yf.contains(this.keys, name.toLowerCase()); + }, + + on: function (pressed) { + /** True if all keys in this binding are pressed. */ + return yf.every.call(pressed, yf.getter, this.keys); + } + }); + + function longerBind (a, b) { + return a.keys.length > b.keys.length ? a : b; + } + + function longestBind (binds) { + return yf.foldl(longerBind, binds); + } + + function isActivate (bind) { + return bind.command[0] === '+' && bind.command[1] !== '+'; + } + + function anticommand (bind) { + return "-" + bind.command.substring(1); + } + + yuu.KeyBindSet = yT({ + constructor: function (binds) { + /** A group of key bindings. + + A set may only have one bind per key combination + (regardless of order) at a time. Binding already-bound + keys to a different command will overwrite, not duplicate, + that binding. + */ + this.binds = []; + yf.ipairs.call(this, this.bind, binds || {}); + }, + + bind: function (keystring, command) { + /** Bind keys to a command in this set. */ + var bind = new KeyBind(keystring, command); + this.binds = this.binds.filter(function (b) { + return !yf.seqEqual(b.keys, bind.keys); + }).concat(bind); + }, + + unbind: function (keystring) { + /** Unbind keys from this set. */ + var keys = splitKeys(keystring); + this.binds = this.binds.filter(function (b) { + return !yf.seqEqual(b.keys, keys); + }); + } + }); + + yuu.keyEventName = function (event) { + return yuu.keyCodeName(event.keyCode, event.key || event.keyIdentifier); + }; + + yuu.keyCodeName = function (code, defaultName) { + if (defaultName) + defaultName = defaultName.toLowerCase(); + if (defaultName === "unidentified") + defaultName = null; + var name = yuu.KEY_NAMES[code] || defaultName; + if (!name) + name = "key:" + code; + return name; + }; + + yuu.InputState = yT({ + constructor: function (bindsets) { + this._bindsets = yf.slice(bindsets || [yuu.defaultKeybinds]); + this.pressed = {}; + /** The current state of each key; 0 if not pressed, + the time it was pressed if it is pressed. */ + }, + + push: function (bindset) { + /** Add a key bind set to the handler stack. */ + this._bindsets = this._bindsets.concat(bindset); + }, + + remove: function (bindset) { + /** Remove a key bind set from the handler stack. */ + this._bindsets = yf.without(this._bindsets, bindset); + }, + + insertBefore: function (bindset, before) { + this._bindsets = yf.insertBefore( + this._bindsets.slice(), bindset, before); + }, + + _triggeredBinds: function (name) { + var pressed = this.pressed; + function triggered (bind) { + return bind.uses(name) && bind.on(pressed); + } + var binds = []; + yf.each(function (bindset) { + binds.push.apply(binds, yf.filter(triggered, bindset.binds)); + }, this._bindsets); + return binds; + }, + + _down: function (name) { + /** Mark the input as down, return an array of commands. + + This array is always of length 1 for down/change events. + + Returns null if no binds were triggered, which is slightly + different than binds being triggered but no commands are + to be executed. + */ + var pressed = this.pressed[name]; + this.pressed[name] = Date.now(); + var bind = longestBind(this._triggeredBinds(name)); + return bind ? pressed ? [] : [bind.command] : null; + }, + + _up: function (name) { + /** Mark the input as down, return an array of commands. + + Only binds for commands with the special + form are + returned on release, and the + is converted to a -. + + Returns null if no binds were triggered, which is slightly + different than binds being triggered but no commands are + to be executed. + */ + if (!this.pressed[name]) + return null; + var cmds = yf.map(anticommand, yf.filter( + isActivate, this._triggeredBinds(name))); + this.pressed[name] = 0; + return cmds.length ? cmds : null; + }, + + change: function (name) { + /** Mark the input as changed, return an array of commands. + + `change` is for inputs that do not have meaningful + "down" or "up" states, like moving a mouse. Instead, + it fires when the input's state changes - e.g. when + the x and y position change. + + The `pressed` table remains unmodified as a result of + `change` inputs. Like `down`, `change` returns only the + first match it finds. + */ + this.pressed[name] = Date.now(); + var bind = longestBind(this._triggeredBinds(name)); + this.pressed[name] = 0; + return bind ? [bind.command] : null; + }, + + keydown: { proxy: "_down" }, + keyup: { proxy: "_up" }, + + gamepadbuttondown: function (gamepad, button) { + return this._down("gamepad" + gamepad.index + "button" + button) + || this._down("gamepadbutton" + button); + }, + + gamepadbuttonup: function (gamepad, button) { + return this._up("gamepad" + gamepad.index + "button" + button) + || this._up("gamepadbutton" + button); + }, + + mousemove: function () { + return this.change("mousemove"); + }, + + mousedown: function (button) { + return this._down("mouse" + button); + }, + + mouseup: function (button) { + return this._up("mouse" + button); + }, + + reset: function () { + this.pressed = {}; + } + }); + + yuu.stopPropagation = function stopPropagation (event, preventDefault) { + event.stopPropagation(); + if (preventDefault) + event.preventDefault(); + if (event.stopImmediatePropagation) + event.stopImmediatePropagation(); + if (event.gesture && event.gesture !== event) + stopPropagation(event.gesture, preventDefault); + }; + + yuu.registerInitHook(function () { + yuu.defaultCommands.bind = yuu.cmd(function (key, command) { + yuu.defaultKeybinds.bind(key, command); + }, " ", "bind a key to a command"); + + yuu.defaultCommands.unbind = yuu.cmd(function (key) { + yuu.defaultKeybinds.unbind(key); + }, "", "unbind a key"); + + yuu.defaultKeybinds = new yuu.KeyBindSet(); + /** The default / debugging bind set */ + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/pre.js b/src/yuu/pre.js new file mode 100644 index 0000000..9fd7f5c --- /dev/null +++ b/src/yuu/pre.js @@ -0,0 +1,338 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +(function () { + "use strict"; + + /** Polyfills and cross-browser fixes */ + + if (!Math.sign) + Math.sign = function (a) { return a && (a > 0) - (a < 0); }; + + if (!String.prototype.repeat) + Object.defineProperty(String.prototype, "repeat", { + value: function (count) { + var string = this.toString(); + var result = ''; + var n = count | 0; + while (n) { + if (n % 2 === 1) + result += string; + if (n > 1) + string += string; + n >>= 1; + } + return result; + } + }); + + if (!String.prototype.startsWith) + Object.defineProperty(String.prototype, "startsWith", { + value: function (sub) { + return this.lastIndexOf(sub, 0) !== -1; + } + }); + + if (!String.prototype.endsWith) + Object.defineProperty(String.prototype, "endsWith", { + value: function (sub) { + return this.indexOf(sub, this.length - sub.length) !== -1; + } + }); + + function toObject (o) { + if (o === null || o === undefined) + throw new TypeError("invalid ToObject cast"); + return Object(o); + } + + if (!Object.assign) + Object.defineProperty(Object, "assign", { + value: function (target) { + target = toObject(target); + for (var i = 1; i < arguments.length; ++i) { + var source = toObject(arguments[i]); + var keys = Object.keys(source); + for (var j = 0; j < keys.length; ++j) + target[keys[j]] = source[keys[j]]; + } + } + }); + + if (!Array.prototype.fill) + Object.defineProperty(Array.prototype, "fill", { + value: function (value) { + var beg = arguments.length > 1 ? +arguments[1] : 0; + var end = arguments.length > 2 ? +arguments[2] : this.length; + if (beg < 0) beg += this.length; + if (end < 0) end += this.length; + for (var i = beg; i < end; ++i) + this[i] = value; + return this; + } + }); + + if (typeof window !== "undefined") { + window.requestAnimationFrame = ( + window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame); + window.cancelAnimationFrame = ( + window.cancelAnimationFrame + || window.mozCancelAnimationFrame + || window.webkitCancelAnimationFrame); + + if (!window.AudioContext) + window.AudioContext = ( + window.webkitAudioContext + || window.mozAudioContext); + + if (window.AudioContext && !window.AudioContext.prototype.createGain) + window.AudioContext.prototype.createGain = + window.AudioContext.prototype.createGainNode; + + /** Canonicalize fullscreen function names if available. + + Based on http://fullscreen.spec.whatwg.org/, June 7th 2013. + */ + + if (!Element.prototype.requestFullscreen) + Element.prototype.requestFullscreen = ( + Element.prototype.requestFullScreen + || Element.prototype.webkitRequestFullscreen + || Element.prototype.webkitRequestFullScreen + || Element.prototype.mozRequestFullScreen + || function () {}); + if (!document.exitFullscreen) + document.exitFullscreen = ( + document.webkitExitFullscreen + || document.webkitCancelFullScreen + || document.mozCancelFullScreen + || function () {}); + if (!document.hasOwnProperty("fullscreenEnabled")) + Object.defineProperty(document, "fullscreenEnabled", { + enumerable: true, + get: function () { + return (this.webkitFullscreenEnabled + || this.mozFullScreenEnabled + || false); + } + }); + if (!document.hasOwnProperty("fullscreenElement")) + Object.defineProperty(document, "fullscreenElement", { + enumerable: true, + get: function () { + return (this.webkitFullscreenElement + || this.webkitCurrentFullScreenElement + || this.mozFullScreenEleement + || null); + } + }); + } + + // Check for Promise.all as Chrome 30 shipped an implementation + // without it and with some other quirks and we don't want to use + // that one. + if (typeof Promise === "undefined" || !Promise.all) (function () { + /* Polyfill based heavily on Christoph Burgmer's ayepromise + + https://github.com/cburgmer/ayepromise/blob/master/ayepromise.js + */ + /* Wrap an arbitrary number of functions and allow only one of + them to be executed and only once */ + function once () { + var wasCalled = false; + + return function (wrappedFunction) { + return function () { + if (wasCalled) { + return; + } + wasCalled = true; + wrappedFunction.apply(null, arguments); + }; + }; + } + + function getThenableIfExists (obj) { + // Make sure we only access the accessor once as required by the spec + var then = obj && obj.then; + + if (typeof obj === "object" && typeof then === "function") + return then.bind(obj); + } + + function aThenHandler (onFulfilled, onRejected) { + var deferred = defer(); + + function doHandlerCall (func, value) { + setTimeout(function () { + var returnValue; + try { + returnValue = func(value); + } catch (e) { + deferred.reject(e); + return; + } + + if (returnValue === deferred.promise) { + deferred.reject(new TypeError()); + } else { + deferred.resolve(returnValue); + } + }, 0); + } + + return { + promise: deferred.promise, + callFulfilled: function (value) { + if (onFulfilled && onFulfilled.call) { + doHandlerCall(onFulfilled, value); + } else { + deferred.resolve(value); + } + }, + callRejected: function (value) { + if (onRejected && onRejected.call) { + doHandlerCall(onRejected, value); + } else { + deferred.reject(value); + } + } + }; + } + + function defer () { + // States + var PENDING = 0, + FULFILLED = 1, + REJECTED = 2; + + var state = PENDING, + outcome, + thenHandlers = []; + + function doFulfill (value) { + state = FULFILLED; + outcome = value; + + thenHandlers.forEach(function (then) { + then.callFulfilled(outcome); + }); + thenHandlers = null; + } + + function doReject (error) { + state = REJECTED; + outcome = error; + + thenHandlers.forEach(function (then) { + then.callRejected(outcome); + }); + thenHandlers = null; + } + + function registerThenHandler (onFulfilled, onRejected) { + var thenHandler = aThenHandler(onFulfilled, onRejected); + + if (state === FULFILLED) { + thenHandler.callFulfilled(outcome); + } else if (state === REJECTED) { + thenHandler.callRejected(outcome); + } else { + thenHandlers.push(thenHandler); + } + + return thenHandler.promise; + } + + function safelyResolveThenable (thenable) { + // Either fulfill, reject or reject with error + var onceWrapper = once(); + try { + thenable( + onceWrapper(transparentlyResolveThenablesAndFulfill), + onceWrapper(doReject) + ); + } catch (e) { + onceWrapper(doReject)(e); + } + } + + function transparentlyResolveThenablesAndFulfill (value) { + var thenable; + + try { + thenable = getThenableIfExists(value); + } catch (e) { + doReject(e); + return; + } + + if (thenable) { + safelyResolveThenable(thenable); + } else { + doFulfill(value); + } + } + + var onceWrapper = once(); + return { + resolve: onceWrapper(transparentlyResolveThenablesAndFulfill), + reject: onceWrapper(doReject), + promise: { + then: registerThenHandler, + "catch": function (onRejected) { + return registerThenHandler(null, onRejected); + } + } + }; + } + + function Promise (callback) { + var deferred = defer(); + try { + callback(deferred.resolve, deferred.reject); + } catch (exc) { + deferred.reject(exc); + } + return deferred.promise; + } + + Promise.resolve = function (v) { + return new Promise(function (resolve) { resolve(v); }); + }; + + Promise.reject = function (error) { + return new Promise(function (_, reject) { reject(error); }); + }; + + Promise.all = function (promises) { + return new Promise(function (resolve, reject) { + var results = []; + var remaining = promises.length; + if (remaining === 0) + return resolve([]); + + promises.forEach(function (promise, i) { + var then = getThenableIfExists(promise); + function resolve1 (value) { + results[i] = value; + if (--remaining === 0) + resolve(results); + } + if (then) { + then.call(promise, resolve1, reject); + } else { + --remaining; + results[i] = promise; + } + }); + }); + }; + + this.Promise = Promise; + }).call(this); +}).call(this); diff --git a/src/yuu/rdr.js b/src/yuu/rdr.js new file mode 100644 index 0000000..a03cbec --- /dev/null +++ b/src/yuu/rdr.js @@ -0,0 +1,426 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + if (!yuu.C) require("./ce"); + if (!yuu.Material) require("./gfx"); + + yuu.Renderable = yT({ + constructor: function (vbuf, primitive, material, uniforms, z) { + this.vbuf = vbuf; + this.primitive = primitive || yuu.gl.TRIANGLES; + this.material = material || yuu.Material.DEFAULT; + this.uniforms = uniforms || {}; + this.z = z || 0.0; + }, + + bind: function () { + this.material.program.setUniforms(this.uniforms); + this.vbuf.bindBuffer(); + this.material.program.setAttribPointers(this.vbuf); + }, + + draw: function () { + this.bind(); + yuu.gl.drawArrays(this.primitive, 0, this.vbuf.vertexCount); + }, + + vertexCount: { alias: "vbuf.vertexCount" } + }); + + yuu.IndexedRenderable = yT(yuu.Renderable, { + constructor: function (vbuf, primitive, material, uniforms, z, ibuf) { + yuu.Renderable.call(this, vbuf, primitive, material, uniforms, z); + this.ibuf = ibuf; + }, + + bind: function () { + yuu.Renderable.prototype.bind.call(this); + this.ibuf.bindBuffer(); + }, + + draw: function () { + this.bind(); + yuu.gl.drawElements( + this.primitive, this.ibuf.length, this.ibuf.GL_TYPE, 0); + }, + + vertexCount: { + get: function () { + return this.ibuf.length; + }, + set: function (vertexCount) { + this.vbuf.vertexCount = vertexCount; + this.ibuf.maxIndex = vertexCount; + } + } + }); + + yuu.Quad = yT({ + /** A vertex view containing a 2D quadrilateral + + You probably don't want to use this directly. If you want a + simple quad, look at QuadC. + */ + constructor: function (vbuf) { + this._vbuf = vbuf; + this.anchor = "center"; + this.position = [0.0, 0.0]; + this.size = [1.0, 1.0]; + this.texBounds = [0.0, 0.0, 1.0, 1.0]; + this.color = [1.0, 1.0, 1.0, 1.0]; + }, + + size: { + get: function () { + var b = this._vbuf.arrays.position; + return [b[6] - b[0], b[4] - b[1]]; + }, + set: function(size) { + var position = this.position; + var b = this._vbuf.arrays.position; + b[0] = b[3] = b[1] = b[7] = 0; + b[6] = b[9] = size[0]; + b[4] = b[10] = size[1]; + this.position = position; + this._vbuf.dirty = true; + } + }, + + position: { + get: function () { + var b = this._vbuf.arrays.position; + return yuu.anchorPoint(this.anchor, b[0], b[1], b[6], b[4]); + }, + set: function (position) { + var size = this.size; + var b = this._vbuf.arrays.position; + var bottomLeft = yuu.bottomLeft( + this.anchor, position[0], position[1], size[0], size[1]); + b[0] = b[3] = bottomLeft[0]; + b[1] = b[7] = bottomLeft[1]; + b[6] = b[9] = bottomLeft[0] + size[0]; + b[4] = b[10] = bottomLeft[1] + size[1]; + this._vbuf.dirty = true; + } + }, + + x: { synthetic: "position[0]" }, + y: { synthetic: "position[1]" }, + + // Texture coordinate vertices: 12 +... + // 2,3 6,7 + // 0,1 4,5 + + texBounds: { + get: function() { + var b = this._vbuf.arrays.texCoord; + return [b[0], b[1], b[6], b[7]]; + }, + set: function (uv0uv1) { + var b = this._vbuf.arrays.texCoord; + b[0] = b[2] = uv0uv1[0]; + b[1] = b[5] = uv0uv1[1]; + b[6] = b[4] = uv0uv1[2]; + b[3] = b[7] = uv0uv1[3]; + this._vbuf.dirty = true; + } + }, + + // Color vertices: 20 +... + // 4,5,6,7 12,13,14,15 + // 0,1,2,3 8,9,10,11 + + color: { + get: function () { + var b = this._vbuf.arrays.color; + return [b[0], b[1], b[2], b[3]]; + }, + set: function (rgba) { + var b = this._vbuf.arrays.color; + var a = rgba[3]; + b[0] = b[4] = b[8] = b[12] = rgba[0]; + b[1] = b[5] = b[9] = b[13] = rgba[1]; + b[2] = b[6] = b[10] = b[14] = rgba[2]; + if (a !== undefined) + b[3] = b[7] = b[11] = b[15] = a; + this._vbuf.dirty = true; + } + }, + + luminance: { + get: function () { + var color = this.color; + return 0.2126 * color[0] + + 0.7152 * color[1] + + 0.0722 * color[2]; + }, + + set: function (v) { + this.color = [v, v, v]; + } + }, + + alpha: { + get: function () { return this._vbuf.arrays.color[3]; }, + set: function (a) { + var b = this._vbuf.arrays.color; + b[3] = b[7] = b[11] = b[15] = a; + this._vbuf.dirty = true; + } + } + }); + + yuu.QuadBatch = yT({ + constructor: function (capacity) { + this.vbuf = new yuu.VertexBuffer(yuu.V3T2C4_F, capacity * 4); + this.ibuf = new yuu.IndexBuffer( + this.vbuf.vertexCount, capacity * 6); + this._capacity = capacity; + this._resetAllocations(); + }, + + _vbufSlotFromQuad: function (quad) { + if (quad._vbuf.arrays.position.buffer !== this.vbuf.buffer) + throw new Error("invalid quad buffer"); + var offset = quad._vbuf.arrays.position.byteOffset; + var bytesPerQuad = ( + this.vbuf.spec.attribs.position.View.BYTES_PER_ELEMENT + * this.vbuf.spec.attribs.position.elements + * 4 /* vertices per quad */); + return offset / bytesPerQuad; + }, + + createQuad: function () { + var slot = this._freeVbufSlots[this._allocated]; + if (slot === undefined) + throw new Error("out of batch slots"); + var subdata = this.vbuf.subdata(slot * 4, 4); + var index = this._allocated++; + var n = 6 * index; + this.ibuf.buffer[n + 0] = slot * 4 + 0; + this.ibuf.buffer[n + 1] = slot * 4 + 1; + this.ibuf.buffer[n + 2] = slot * 4 + 2; + this.ibuf.buffer[n + 3] = slot * 4 + 2; + this.ibuf.buffer[n + 4] = slot * 4 + 1; + this.ibuf.buffer[n + 5] = slot * 4 + 3; + this.ibuf.length += 6; + this.ibuf.dirty = true; + this._vbufToIndex[slot] = index; + return new yuu.Quad(subdata); + }, + + disposeQuad: function (quad) { + var slot = this._vbufSlotFromQuad(quad); + var index = this._vbufToIndex[slot]; + this._allocated--; + if (index !== this._allocated) { + // Unless this was the last index, swap the last index + // into the new hole. + var n = 6 /* indices per quad */ * index; + var m = 6 /* indices per quad */ * this._allocated; + var b = this.ibuf.buffer; + var lastVbufSlot = b[m] / 4 /* vertices per quad */; + if (this._vbufToIndex[lastVbufSlot] !== this._allocated) + throw new Error("allocation index mismatch"); + b[n + 0] = b[m + 0]; + b[n + 1] = b[m + 1]; + b[n + 2] = b[m + 2]; + b[n + 3] = b[m + 3]; + b[n + 4] = b[m + 4]; + b[n + 5] = b[m + 5]; + this.ibuf.dirty = true; + this._vbufToIndex[lastVbufSlot] = index; + } + this._freeVbufSlots[this._allocated] = slot; + this.ibuf.length -= 6; + }, + + _resetAllocations: function () { + this.ibuf.length = 0; + var Array = yuu.IndexBuffer.Array(this._capacity); + this._freeVbufSlots = new Array(this._capacity); + yf.transform(yf.counter(), this._freeVbufSlots); + this._allocated = 0; + this._vbufToIndex = new Array(this._capacity); + }, + + disposeAll: function () { + this._resetAllocations(); + } + }); + + yuu.QuadC = yT(yuu.C, { + /** A 2D quadrilateral that tracks the entity's transform + + By default, the extents of this quad are [-0.5, -0.5] to + [0.5, 0.5], and its model matrix is identical to the + entity's transform, i.e. it is centered around [0, 0] in + the entity's local space, or the entity's nominal location + in world space. This can be changed by adjusting the + anchor, position, and size properties. + */ + + constructor: function (material) { + var buffer = new yuu.VertexBuffer(yuu.V3T2C4_F, 4); + this._quad = new yuu.Quad(buffer); + this._rdro = new yuu.Renderable( + buffer, yuu.gl.TRIANGLE_STRIP, material, + { model: mat4.create() }, 0.0); + }, + + TAPS: ["queueRenderables"], + + queueRenderables: function (rdros) { + mat4.copy(this._rdro.uniforms.model, + this.entity.transform.matrix); + rdros.push(this._rdro); + }, + + // TODO: yT should offer some way to specify these in two + // lists, i.e. the rdro aliases, and the quad aliases. + + material: { alias: "_rdro.material", chainable: true }, + z: { alias: "_rdro.z", chainable: true }, + uniforms: { alias: "_rdro.uniforms" }, + size: { alias: "_quad.size", chainable: true }, + position: { alias: "_quad.position", chainable: true }, + anchor: { alias: "_quad.anchor", chainable: true }, + xy: { alias: "_quad.position", chainable: true }, + texBounds: { alias: "_quad.texBounds", chainable: true }, + color: { alias: "_quad.color", chainable: true }, + alpha: { alias: "_quad.alpha", chainable: true }, + luminance: { alias: "_quad.luminance", chainable: true }, + }); + + yuu.QuadBatchC = yT(yuu.C, { + /** A 2D quadrilateral batch that tracks the entity's transform + + */ + + constructor: function (capacity, material) { + this._batch = new yuu.QuadBatch(capacity); + this._rdro = new yuu.IndexedRenderable( + this._batch.vbuf, yuu.gl.TRIANGLES, material, + { model: mat4.create() }, 0.0, this._batch.ibuf); + }, + + TAPS: ["queueRenderables"], + + queueRenderables: function (rdros) { + mat4.copy(this._rdro.uniforms.model, + this.entity.transform.matrix); + rdros.push(this._rdro); + }, + + material: { alias: "_rdro.material", chainable: true }, + z: { alias: "_rdro.z", chainable: true }, + uniforms: { alias: "_rdro.uniforms" }, + createQuad: { proxy: "_batch.createQuad" }, + disposeQuad: { proxy: "_batch.disposeQuad" }, + disposeAll: { proxy: "_batch.disposeAll" }, + }); + + function sortRenderables(a, b) { return a.z - b.z; } + + yuu.Layer = yT({ + /** List of renderables and per-layer uniforms + + These uniforms usually include the projection and view + matrices, set to a [-1, 1] orthographic projection and the + identity view by default. + */ + + // TODO: This is a bad design. Too powerful to be efficient or + // a straightforward part of Scene; not enough to abstract + // hard things like render passes. + + constructor: function () { + this.rdros = []; + this.uniforms = { + projection: mat4.ortho(mat4.create(), -1, 1, -1, 1, -1, 1), + view: mat4.create() + }; + }, + + worldFromDevice: yf.argcd( + function (p) { + var t = this.worldFromDevice(p.x || p.pageX || p[0] || 0, + p.y || p.pageY || p[1] || 0); + t.inside = p.inside; + return t; + }, + function (x, y) { + var p = { 0: x, 1: y }; + var m = mat4.mul(mat4.create(), + this.uniforms.projection, this.uniforms.view); + m = mat4.invert(m, m); + vec2.transformMat4(p, p, m); + p.x = p[0]; p.y = p[1]; + return p; + } + ), + + worldFromCanvas: yf.argcd( + function (p) { + return this.worldFromDevice(yuu.deviceFromCanvas(p)); + }, + function (x, y) { + return this.worldFromDevice(yuu.deviceFromCanvas(x, y)); + } + ), + + resize: function (x, y, w, h) { + /** Set a 2D orthographic project with an origin and size + + Arguments: + scene.resize(originX, originY, width, height) + scene.resize(width, height) // Origin at 0, 0 + scene.resize(origin, size) + scene.resize(size) // Origin at 0, 0 + */ + if (y === undefined) { + w = x[0]; h = x[1]; x = y = 0; + } else if (w === undefined) { + if (x.length === undefined) { + w = x; h = y; x = y = 0; + } else { + w = y[0]; h = y[1]; + y = x[1]; x = x[0]; + } + } + mat4.ortho(this.uniforms.projection, x, x + w, y, y + h, -1, 1); + }, + + render: function () { + /** Render all queued renderables */ + this.rdros.sort(sortRenderables); + var mat = null; + for (var j = 0; j < this.rdros.length; ++j) { + var rdro = this.rdros[j]; + if (mat !== rdro.material) { + if (mat) + mat.disable(); + mat = rdro.material; + rdro.material.enable(this.uniforms); + } + rdro.draw(); + } + }, + + clear: function () { + this.rdros.length = 0; + } + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/storage.js b/src/yuu/storage.js new file mode 100644 index 0000000..1ad702e --- /dev/null +++ b/src/yuu/storage.js @@ -0,0 +1,188 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (exports) { + "use strict"; + + var yT = this.yT || require('./yT'); + + var FakeStorage = exports.FakeStorage = yT({ + /** Fake, ephemeral storage roughly like Web Storage + + This is the fallback storage when permission is denied or + otherwise busted. It just stores a dictionary for as long as + the object survives. + */ + constructor: function () { + this._storage = {}; + }, + + getItem: function (key) { + return (key in this._storage) ? this._storage[key] : null; + }, + + setItem: function (key, value) { + this._storage[key] = value.toString(); + }, + + removeItem: function (key) { + delete this._storage[key]; + }, + + clear: function () { + this._storage = {}; + }, + + length: { + get: function () { return Object.keys(this._storage).length; } + }, + + key: function (n) { + // Object.keys isn't guaranteed to have a consistent order + // even when nothing changes, so normalize it by sorting. + var keys = Object.keys(this._storage).sort(); + return (n >= 0 && n < keys.length) ? keys[n] : null; + } + }); + + var PrefixedStorage = exports.PrefixedStorage = yT({ + /** Per-application storage roughly like Web Storage + + This storage prefixes all keys with a special token, so you + can run multiple applications on the same origin without + the risk of conflicting keys. + + A caveat of this approach is clear() is not atomic. + */ + + constructor: function (storage, prefix) { + this._storage = storage; + this._prefix = prefix + " -- "; + }, + + _key: function (key) { + return this._prefix + key; + }, + + _unkey: function (key) { + return key.substring(this._prefix.length); + }, + + _iskey: function (key) { + return key.startsWith(this._prefix); + }, + + _keys: function () { + var keys = []; + var key; + var i = 0; + while ((key = this._storage.key(i++)) !== null) + if (this._iskey(key)) + keys.push(this._unkey(key)); + return keys; + }, + + getItem: function (key) { + return this._storage.getItem(this._key(key)); + }, + + setItem: function (key, value) { + return this._storage.setItem(this._key(key), value); + }, + + removeItem: function (key) { + return this._storage.removeItem(this._key(key)); + }, + + clear: function () { + this._keys().forEach(this.removeItem, this); + }, + + length: { + get: function () { return this._keys().length; } + }, + + key: function (n) { + var keys = this._keys().sort(); + return (n >= 0 && n < keys.length) ? keys[n] : null; + } + }); + + var Storage = exports.Storage = yT({ + /** Higher-level access to Web Storage-esque things + + Storage lets you store and retrieve JSON-serializable + objects inside a Web Storage container. + + You can specify default values. If you retrieve an object + that hasn't been set, you get its default value. + + Storage automatically falls back to an ephemeral storage + backend if a SecurityException occurs during startup. + */ + + constructor: function (storage, defaults) { + this._storage = storage || new FakeStorage(); + this._defaults = defaults || {}; + + try { + this.setFlag('__ystorage__'); + } catch (exc) { + this._storage = new FakeStorage(); + console.error("Unable to use provided storage:", exc); + } + }, + + getObject: function (key, fallbackValue) { + var v = this._storage.getItem(key); + if (v === null) { + return (key in this._defaults) + ? this._defaults[key] + : fallbackValue; + } + try { + return JSON.parse(v); + } catch (exc) { + console.error("Malformed storage value:", key, v, exc); + return (key in this._defaults) + ? this._defaults[key] + : fallbackValue; + } + }, + + setObject: function (key, value) { + this._storage.setItem(key, JSON.stringify(value)); + }, + + removeObject: { proxy: '_storage.removeItem' }, + + getFlag: function (key) { + return !!this.getObject(key, false); + }, + + setFlag: function (key) { + return this.setObject(key, true); + }, + + clearFlag: function (key) { + return this.setObject(key, false); + }, + + clear: { proxy: '_storage.clear' } + }); + + exports.getStorage = function (prefix, defaults, backend) { + /** Create a Storage with prefixed access to localStorage. */ + prefix = prefix + || (document && + (document.documentElement.getAttribute('data-appid') + || document.title)); + backend = backend || localStorage; + return new Storage(new PrefixedStorage(backend, prefix), defaults); + }; + +}).call(typeof exports === 'undefined' ? this : exports, + typeof exports === 'undefined' ? (this.ystorage = {}) : exports); diff --git a/src/yuu/yT.js b/src/yuu/yT.js new file mode 100644 index 0000000..0c86c89 --- /dev/null +++ b/src/yuu/yT.js @@ -0,0 +1,364 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (module) { + "use strict"; + + /** yT - yuu type creation + + yT is a function like `Object.create`, but with support for + more powerful property descriptors (referred to as _extended + property descriptors (XPDs)_). Most standard JavaScript + property descriptors are valid XPDs, but XPDs allow shortcuts + to specify common descriptor patterns. + + Equivalents for `Object.defineProperties` and + `Object.defineProperty` are also provided. + + ## Extended Property Descriptors + + Any standard descriptor that has a `get` function (called an + 'accessor descriptor') or a `value` property (called a 'data + descriptor') is also valid XPD. + + An extended descriptor that does not have either of these and + does not meet any of the other conditions below is equivalent + to a data descriptor with a `value` of itself, referred to as + a 'bare value descriptor'. For example, the following two XPDs + are equivalent: + + { x: 1 } { x: { value: 1 } } + + (This means a useless descriptor like `{}` is interpreted as a + data descriptor with value `undefined` by `Object.create` but + a bare value descriptor with value `{}` by `yT`.) + + In addition, extended descriptors have several other formats + which can be used to generate different kinds of idomatic + accessors: + + * `alias` - This property is a synonym for a different property. + Reads and writes to it will be mapped to reads and writes + to the aliased property. + { firstChild: { alias: "children[0]" } } + is equivalent to + { firstChild: { + get: function () { return this.children[0]; }, + set: function (v) { this.children[0] = v; } + } } + + * `proxy` - This property is a synonym for a method call + on a different object. + { start: { proxy: "engine.start" } } + is equivalent to + { start: { value: function () { + return this.engine.start.apply(this.engine, arguments); + } } } + + Using `alias` rather than `proxy` would result in the + wrong (non-engine) `this` argument being passed to the + `start` method. + + * `aliasSynthetic` - Aliases don't work if one of the + properties in the lookup chain is a temporary variable. + For example, aliasing `x` to `position[0]` is no good if + `position` itself has a getter like `transform.slice(12)` + because the assignment to the returned value will have + no effect. + + `aliasSynthetic` can be used to capture the temporary, + assign to it, and then assign the whole temporary back. + { x: { aliasSynthetic: "position[0]" } } + Generates the same `get` as `alias`, but `set` is + function (v) { + var t = this.position; + t[0] = v; + this.position = t; + } + + `aliasSynthetic` assumes the next-to-last value is the + temporary, e.g. in `a.b.c.d`, `a.b.c` is the temporary. + If rather e.g. `a.b` is the temporary, you can separate + the `alias` and `synthetic` parts: + { x: { alias: "a.b.c.d", synthetic: "a.b" } } + + * `swizzle` - Swizzling lets you treat separate properties + as one array property. For example if you have a color + class with individual r, g, and b properties, + { rgb: { swizzle: ["r", "g", "b"] } } + is equivalent to + { rgb: { + get: function () { return [this.r, this.g, this.b] }, + set: function (v) { + this.r = v[0]; + this.g = v[1]; + this.b = v[2]; + } + } } + + Any descriptor may also have the `chainable` property set, + which generates a chainable setter function. Chainable data + descriptors are writable by default. + { x: { value: 0, chainable: true } } + is equivalent to + { x: { value: 0, writable: true }, + setX: { value: function (v) { this.x = v; return this; } } + } + + ## Example + + An example of a simple 2D Point class using XPDs: + + var Point = yT(Object, { + constructor: function (x, y) { + this.x = x || 0; + this.y = y || 0; + }, + + 0: { alias: "x" }, + 1: { alias: "y" }, + xy: { swizzle: "xy" }, + yx: { swizzle: "yx" }, + + angle: { + chainable: true, + get: function () { + return Math.atan2(this.y, this.x); + }, + set: function (angle) { + var magnitude = this.magnitude; + this.x = Math.cos(angle) * magnitude; + this.y = Math.sin(angle) * magnitude; + } + }, + + magnitude: { + chainable: true, + get: function () { + return Math.sqrt(this.x * this.x + this.y * this.y); + }, + set: function (magnitude) { + var angle = this.angle; + this.x = Math.cos(angle) * magnitude; + this.y = Math.sin(angle) * magnitude; + } + }, + + length: 2 + }); + + var p = new Point(3, 0); + p[0] === 3; // true + p.y = 4; p[1] == 4; // true + p.magnitude = 1; // normalize + p.xy = p.yx; // transpose + + new Point().setMagnitude(m).setAngle(a); + // construct a point from an angle and magnitude in a + // single step. + */ + + /* jshint -W054 */ // Function constructors are the whole point here. + + function isFunction (o) { + /** Check if a value is a function */ + return Object.prototype.toString.call(o) === '[object Function]'; + } + + function update (dst, src) { + /** Copy every enumerable key and its value from src to dst */ + for (var k in src) + dst[k] = src[k]; + return dst; + } + + function rooted (path) { + if (typeof path === "number") + return "[" + path + "]"; + else if (path[0] !== "." && path[0] !== "[") + return "." + path; + else + return path; + } + + function chainableName (name) { + return "set" + name[0].toUpperCase() + name.substring(1); + } + + function chainableSetter (name) { + return new Function( + "value", + "this" + rooted(name) + " = value; " + "return this;"); + } + + function alias (path, readonly) { + path = rooted(path); + return readonly + ? { get: new Function("return this" + path + ";") } + : { get: new Function("return this" + path + ";"), + set: new Function("v", "return this" + path + " = v;") }; + } + + function proxy (path) { + path = rooted(path); + var prop = path.substr(0, path.lastIndexOf(".")); + return { + value: new Function( + "return this" + path + ".apply(this" + prop + ", arguments);") + }; + } + + function swizzle (props) { + props = Array.prototype.map.call(props, function (p) { + return "this" + rooted(p); + }); + var assignments = props.map(function (p, i) { + return p + " = x[" + i + "];"; + }); + return { + get: new Function("return [" + props.join(", ") + "];"), + set: new Function("x", assignments.join("\n")) + }; + } + + function aliasSynthetic (path) { + var idx = Math.max(path.lastIndexOf("."), path.lastIndexOf("[")); + return synthetic(path.substring(0, idx), path); + } + + function synthetic (synthPath, path) { + synthPath = rooted(synthPath); + path = rooted(path).substring(synthPath.length); + return { + get: new Function("return this" + synthPath + path + ";"), + set: new Function("v", + ["var t = this" + synthPath + ";", + "t" + path + " = v; ", + "this" + synthPath + " = t;", + ].join("\n")) + }; + } + + function isAccessorDescriptor (pd) { + /** Check if `pd` is a descriptor describing an accessor */ + return pd && typeof pd === "object" && isFunction(pd.get); + } + + function isDataDescriptor (pd) { + /** Check if `pd` is a descriptor describing data */ + return pd && typeof pd === "object" && ("value" in pd); + } + + function isDescriptor (pd) { + /** Check if `pd` is any kind of descriptor */ + return isDataDescriptor(pd) || isAccessorDescriptor(pd); + } + + function createDescriptors (name, xpd, pds) { + if (xpd.alias !== undefined) { + if (xpd.synthetic !== undefined) + pds[name] = update(xpd, synthetic(xpd.alias, xpd.synthetic)); + else + pds[name] = update(xpd, alias(xpd.alias, xpd.readonly)); + } else if (xpd.proxy !== undefined) { + pds[name] = update(xpd, proxy(xpd.proxy)); + } else if (xpd.swizzle !== undefined) { + pds[name] = update(xpd, swizzle(xpd.swizzle)); + } else if (xpd.aliasSynthetic !== undefined) { + pds[name] = update(xpd, aliasSynthetic(xpd.aliasSynthetic)); + } + + pds[name] = isDescriptor(xpd) ? xpd : { value: xpd }; + if (xpd.chainable) { + pds[chainableName(name)] = { value: chainableSetter(name) }; + if (isDataDescriptor(pds[name]) && !("writable" in pds[name])) + pds[name].writable = true; + } + } + + function xpdsToPds (xpds) { + var pds = {}; + Object.keys(xpds).forEach(function (k) { + createDescriptors(k, xpds[k], pds); + }); + return pds; + } + + function T (parent, xpds) { + /** Create a new class described by XPDs + + This function is similar to `Object.create` but supports + extended property descriptors as described above. + + yT(parent, map of extended descriptors) + yT(map of extended descriptors) + // The parent is assumed to be Object + + This returns a _function_ which acts as a constructor for + the provided type. If there is a property descriptor named + `constructor` it is returned; otherwise a new function + that calls the parent's. + + `parent` may be either the parent constructor or a + prototype. + */ + if (!xpds) { + xpds = parent; + parent = Object; + } + parent = isFunction(parent) && parent.prototype || parent; + var pds = xpdsToPds(xpds); + var ctor = pds.constructor && pds.constructor.value; + if (!ctor || ctor === {}.constructor) { + ctor = parent + ? function () { parent.constructor.apply(this, arguments); } + : function () { }; + pds.constructor = { value: ctor }; + } + ctor.prototype = Object.create(parent, pds); + return ctor; + } + + T.defineProperties = function (o, xpds) { + /** Add properties described by XPDs to an object + + This function is similar to`Object.defineProperties`, + but supports the same features as `yT`. + */ + return Object.defineProperties(o, xpdsToPds(xpds)); + }; + + T.defineProperty = function (o, name, xpd) { + /** Add a property described by an XPD to an object + + This function is similar to`Object.defineProperty`, + but supports the same features as `yT`. + */ + var xpds = {}; + xpds[name] = xpd; + return T.defineProperties(o, xpds); + }; + + T.getPropertyDescriptor = function (o, name) { + /** Look up a descriptor from `o`'s prototype chain */ + var v = null; + while (!v && o) { + v = Object.getOwnPropertyDescriptor(o, name); + o = Object.getPrototypeOf(o); + } + return v; + }; + + T.isAccessorDescriptor = isAccessorDescriptor; + T.isDataDescriptor = isDataDescriptor; + T.isDescriptor = isDescriptor; + + if (module) + module.exports = T; + else + this.yT = T; +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" ? null : module); diff --git a/src/yuu/yf.js b/src/yuu/yf.js new file mode 100644 index 0000000..5df966d --- /dev/null +++ b/src/yuu/yf.js @@ -0,0 +1,578 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function () { + "use strict"; + + /** yf - yuu foundation + + A collection of tools for functional programming and the kind + of sequence (array) manipulations usually associated with + functional programming. + + yf doesn't depend on yuu specifically, but may use standard + features not yet widely supported provided by yuu/pre. + */ + + function isFunction (o) { + /** Check if a value is a function */ + return Object.prototype.toString.call(o) === '[object Function]'; + } + + function isString (s) { + /** Check if a value is a string */ + return Object.prototype.toString.call(s) === "[object String]"; + } + + function I (x) { return x; } + + function K (x) { return function () { return x; }; } + + /** Get the minimum or maximum of two objects + + The default JavaScript Math.min and Math.max are unsuitable as + generic functions. They don't work on strings. Because they + support a variable numbers of arguments they don't work + correctly when passed directly to higher-order functions that + pass "bonus" arguments such as Array.prototype.reduce. + + These functions work on any objects comparable with < and >, + and only consider two arguments. + */ + function min (a, b) { return b < a ? b : a; } + function max (a, b) { return b > a ? b : a; } + function clamp(x, lo, hi) { return max(lo, min(hi, x)); } + + /** Get a property from the this object by name */ + function getter (key) { return this[key]; } + + /** Set a property value on the this object by name */ + function setter (key, value) { this[key] = value; } + + function unbind (f) { + /** Factor out the special 'this' argument from a function + + unbind(f)(a, b, c) is equivalent to f.call(a, b, c), and + you may store the return value for later use without + storing the original function. + */ + + // return f.call.bind(f) is ~15% slower than this closure on + // Chrome 31 and negligibly slower on Firefox 25. + return function () { + return f.call.apply(f, arguments); + }; + } + + /** Unbound versions of useful functions */ + var slice = unbind(Array.prototype.slice); + + function contains (seq, o) { + return Array.prototype.indexOf.call(seq, o) !== -1; + } + + function lacks (seq, o) { + return !contains(seq, o); + } + + /** The first element of a sequence */ + function head (a) { return a[0]; } + + /** All but the first element of a sequence */ + function tail (a) { return slice(a, 1); } + + /** The last element of a sequence */ + function last (a) { return a[a.length - 1]; } + + /** All but the last element of a sequence */ + function most (a) { return slice(a, 0, -1); } + + /** The length of a sequence */ + function len (seq) { return seq.length; } + + /** Note that yf's argument order is slightly different from + JavaScript's normal order (but the same as most other + languages with functional elements). + + This is irrelevant for `fold` and `filter`, but `each` and + `map` can take multiple sequences to allow callback functions + with arbitrary arity. + + Because of this difference, there is no final `thisArg` as in + e.g. the standard Array's forEach. Instead the `this` of + the original call is forwarded. For example, + someArray.forEach(callback, thisArg) + becomes + yf.each.call(thisArg, callback, someArray) + */ + + function foldl (callback, seq, value) { + /** Like Array's reduce, but with no extra arguments + + This traverses the sequence in indexed order, low-to-high, + and calls the provided function with the accumulated value + and the sequence item. + */ + var i = 0; + if (arguments.length === 2) + value = seq[i++]; + for (; i < seq.length; ++i) + value = callback.call(this, value, seq[i]); + return value; + } + + function foldr (callback, seq, value) { + /** Like foldl, but reversed. + + This traverses the sequence in indexed order, high-to-low, + and calls the provided function with the sequence item and + the accumulated value. + */ + var i = seq.length - 1; + if (arguments.length === 2) + value = seq[i--]; + for (; i >= 0; --i) + value = callback.call(this, seq[i], value); + return value; + } + + function call (f, x) { return f.call(this, x); } + + function compose () { + /** Create a function by composing multiple functions + + compose(f, g, h)(x, y, z) is equivalent to f(g(h(x, y, z))). + */ + var inner = last(arguments); + var funcs = most(arguments); + return function () { + return foldr.call(this, call, funcs, inner.apply(this, arguments)); + }; + } + + // Generating specialized functions for plucking the nth argument + // and passing it to a callback is 100-120x faster than the + // generic one in V8. + // + // So generate specialized functions for small argc, and only + // resort to the generic approach when there's many sequences. + var CALLS = []; + + /* jshint -W054*/ /* Function constructor is a form of eval */ + for (var args = []; args.length < 32; + args.push(", seqs[" + args.length + "][i]")) + CALLS[args.length] = new Function( + "callback", "i", "seqs", + "return callback.call(this" + args.join("") + ");"); + + function lookup (prop) { + /** Return a function that gets a particular property */ + + // Building this as a closure is ~2-5x faster than writing + // a function lookup(prop, o) and binding it. + return function (s) { return s[prop]; }; + } + + function callx (callback, i, seqs) { + return callback.apply(this, seqs.map(lookup(i))); + } + + function calln (argc) { + return CALLS[argc] || callx; + } + + function argv (f) { + /** Provide a variadic-like version of f + + Arguments passed to the function including and after the + final named one are collected and passed as a single + sequence (not necessarily Array) value. + */ + var length = f.length; + switch (length) { + case 0: throw new Error("don't use argv for 0-ary functions"); + case 1: return function () { return f.call(this, arguments); }; + default: + return function () { + arguments[length - 1] = slice(arguments, length - 1); + return f.apply(this, arguments); + }; + } + } + + var each = argv(function (callback, seqs) { + /** Call a function on the given sequences' elements + + If one sequence is longer than the others, `undefined` + will be passed for that sequence's argument. + + For example, each(f, [a, b, c], [x, y]) is equivalent to + f(a, x); + f(b, y); + f(c, undefined); + + If you want to pass a thisArg to the callback, use + each.call(thisArg, callback, ...). + */ + var length = Math.max.apply(Math, seqs.map(len)); + var fn = calln(seqs.length); + for (var i = 0; i < length; ++i) + fn.call(this, callback, i, seqs); + }); + + var eachr = argv(function (callback, seqs) { + /** Call a function on the given sequences' elements, backwards + + If one sequence is longer than the others, `undefined` will be + passed for that sequence's argument. + + For example, each(f, [a, b, c], [x, y]) is equivalent to + f(c, undefined); + f(b, y); + f(a, x); + + If you want to pass a thisArg to the callback, use + eachr.call(thisArg, callback, ...). + */ + var length = Math.max.apply(Math, seqs.map(len)); + var fn = calln(seqs.length); + for (var i = length - 1; i >= 0; --i) + fn.call(this, callback, i, seqs); + }); + + function pusher (a) { + // Ridiculously faster than a.push.bind(a). :( + return function (o) { a.push(o); }; + } + + function map () { + /** Build an array by applying a function to sequences + + Similar to Array.prototype.map, but allows passing multiple + sequences to call functions of any arity, as with each. + + If you want to pass a thisArg to the callback, use + map.call(thisArg, callback, ...). + */ + var a = []; + arguments[0] = compose(pusher(a), arguments[0]); + each.apply(this, arguments); + return a; + } + + function mapr () { + /** Build an array by applying a function to sequences backwards + + As eachr is to each, so mapr is to map. + */ + var a = []; + arguments[0] = compose(pusher(a), arguments[0]); + eachr.apply(this, arguments); + return a; + } + + function filter (callback, seq) { + /** Build an array without the elements that fail the predicate + + Like Array.prototype.filter, but if null is passed for the + predicate, any false-y values will be removed. + */ + var a = []; + callback = callback || I; + function _ (o) { if (callback.call(this, o)) a.push(o); } + each.call(this, _, seq); + return a; + } + + function packed (f) { + /** Pack arguments to a function into an Array + + packed(f)([a, b, c]) is equivalent to f(a, b, c). + */ + return function (args) { return f.apply(this, args); }; + } + + function argcd () { + /** Create a function that dispatches based on argument count + + Pass a list of functions with varying argument lengths, e.g. + var f = argcd(function () { ... }, + function (a, b) { ... }, + function (a, b, c) { ... }); + + This can be used for function overloading, clearer + defaults for positional parameters, or significantly more + evil things. + + The functions can also be accessed directly by their + argument count, e.g. f[0], f[2], f[3] above. + + If no matching argument count is found for a call, a + TypeError is raised. + */ + if (arguments.length === 1) + throw new Error("don't use argcd for one function"); + var table = (function table () { + return table[arguments.length].apply(this, arguments); + }); + for (var i = 0; i < arguments.length; ++i) + table[arguments[i].length] = arguments[i]; + return table; + } + + var irange = argcd( + /** Call the provided callback counting as it goes + + irange(x) counts from [0, x), by 1. + + irange(x, y) counts from [x, y), by 1. + + irange(x, y, s) counts from [x, y), by s. + */ + function (callback, stop) { + return irange.call(this, callback, 0, stop, 1); + }, + function (callback, start, stop) { + return irange.call(this, callback, start, stop, 1); + }, + function (callback, start, stop, step) { + var length = Math.max(Math.ceil((stop - start) / step), 0); + for (var i = 0; i < length; ++i) + callback.call(this, start + step * i); + } + ); + + function capture1 (ifunc) { + /** Build an array from the values of an iterating function + + The elements added to the array are the first arguments + the iterating function passes. + */ + return function () { + var a = []; + var args = slice(arguments); + args.unshift(pusher(a)); + ifunc.apply(this, args); + return a; + }; + } + + /** Generate an Array from range of numbers, as irange */ + var range = capture1(irange); + + function ipairs (callback, o) { + /** Call the provided callback with each key and value of the object */ + each.call(this, function (k) { callback.call(this, k, o[k]); }, + Object.keys(o)); + } + + function iprototypeChain (callback, o) { + /** Traverse an object and its prototype chain */ + do { + callback.call(this, o); + } while ((o = Object.getPrototypeOf(o))); + } + + function allKeys (o) { + /** Get *ALL* property names. Even unowned non-enumerable ones. */ + var props = []; + iprototypeChain(compose(packed(props.push).bind(props), + Object.getOwnPropertyNames), + o); + return props.sort().filter(function (p, i, a) { + return p !== a[i - 1]; + }); + } + + var some = argv(function (callback, seqs) { + callback = callback || I; + var length = Math.max.apply(Math, seqs.map(len)); + var fn = calln(seqs.length); + for (var i = 0; i < length; ++i) + if (fn.call(this, callback, i, seqs)) + return true; + return false; + }); + + var eachrUntil = argv(function (callback, seqs) { // Reverse of `some` + callback = callback || I; + var length = Math.max.apply(Math, seqs.map(len)); + var fn = calln(seqs.length); + for (var i = length - 1; i >= 0; --i) + if (fn.call(this, callback, i, seqs)) + return true; + return false; + }); + + function not (f) { + /** Build a function equivalent to !f(...) */ + return function () { return !f.apply(this, arguments); }; + } + + function every () { + arguments[0] = not(arguments[0] || I); + return !some.apply(this, arguments); + } + + function none () { + return !some.apply(this, arguments); + } + + function repeat (o, n) { + return (new Array(n)).fill(o); + } + + function arrayify (o) { + /** Equivalent to [].concat(o) but faster */ + return Array.isArray(o) ? o : [o]; + } + + function stash (key, value, a) { + each(function (o) { o[key] = value; }, a); + } + + function without (seq, o) { + if (arguments.length === 2) + return filter(function (a) { return a !== o; }, seq); + var os = slice(arguments, 1); + return filter(lacks.bind(null, os), seq); + } + + function construct (ctr, args) { + args = slice(args); + args.unshift(ctr); + return new (Function.prototype.bind.apply(ctr, args))(); + } + + function new_ (ctr) { + return function () { return construct(ctr, arguments); }; + } + + function counter (start) { + var i = +(start || 0); + return function () { return i++; }; + } + + function volatile (f) { + return { valueOf: f }; + } + + function transform (callback, seq) { + irange.call(this, function (i) { + seq[i] = callback.call(this, seq[i]); + }, seq.length); + } + + function mapValues (callback, o) { + var r = {}; + for (var k in o) + r[k] = callback.call(this, o[k]); + return r; + } + + function insertBefore (seq, o, before) { + var idx = seq.lastIndexOf(before); + if (idx === -1) + seq.push(o); + else + seq.splice(idx, 0, o); + return seq; + } + + function seqEqual (a, b) { + if (a.length !== b.length) + return false; + for (var i = 0; i < a.length; ++i) + if (a[i] !== b[i]) + return false; + return true; + } + + function debounce (callback, wait) { + wait = wait || 100; + var this_ = null; + var args = null; + var id = null; + + function _ () { + callback.apply(this_, args); + this_ = null; + args = null; + id = null; + } + + return function () { + if (id !== null && this_ !== this) + _(); + clearTimeout(id); + this_ = this; + args = arguments; + id = setTimeout(_, wait); + }; + } + + function pluck (prop, seq) { + return map(lookup(prop), seq); + } + + Object.assign(this, { + allKeys: allKeys, + argcd: argcd, + argv: argv, + arrayify: arrayify, + clamp: clamp, + compose: compose, + construct: construct, + contains: contains, + counter: counter, + debounce: debounce, + each: each, + eachUntil: some, + eachr: eachr, + eachrUntil: eachrUntil, + every: every, + filter: filter, + foldl: foldl, + foldr: foldr, + getter: getter, + head: head, + I: I, + insertBefore: insertBefore, + ipairs: ipairs, + irange: irange, + isFunction: isFunction, + isString: isString, + K: K, + lacks: lacks, + last: last, + len: len, + lookup: lookup, + map: map, + mapr: mapr, + mapValues: mapValues, + max: max, + min: min, + most: most, + new_: new_, + none: none, + pluck: pluck, + range: range, + repeat: repeat, + seqEqual: seqEqual, + setter: setter, + slice: slice, + some: some, + stash: stash, + tail: tail, + transform: transform, + unbind: unbind, + volatile: volatile, + without: without, + + VERSION: 0 + }); + +}).call(typeof exports !== "undefined" ? exports : (this.yf = {})); diff --git a/test/jshint.config b/test/jshint.config new file mode 100644 index 0000000..3a5ca20 --- /dev/null +++ b/test/jshint.config @@ -0,0 +1,34 @@ +{ + "browser": true, + "laxbreak": true, + "globalstrict": true, + "validthis": true, + "devel": true, + "unused": "vars", + "camelcase": true, + "eqeqeq": true, + "latedef": true, + "nonew": true, + "undef": true, + "trailing": true, + "globals": { + "Event": false, + "Promise": false, + "Hammer": false, + "Buffer": false, + "gl": false, + "yf": false, + "yT": false, + "yuu": false, + "ystorage": false, + "mat4": false, + "vec3": false, + "vec2": false, + "quat": false, + "exports": false, + "module": false, + "process": false, + "require": false, + "escape": true + } +} diff --git a/test/spec/yuu/core.js b/test/spec/yuu/core.js new file mode 100644 index 0000000..13098e2 --- /dev/null +++ b/test/spec/yuu/core.js @@ -0,0 +1,105 @@ +var JS = this.JS || require('jstest'); +var yuu = require('yuu/core'); + +JS.Test.describe('yuu core', function () { with (this) { + it("knows signum", function () { with (this) { + assertEqual(-1, Math.sign(-10)); + assertEqual(1, Math.sign(10)); + assertEqual(0, Math.sign(0)); + }}); + it("knows signum for weird numbers", function () { with (this) { + assertEqual(-1, Math.sign(-Infinity)); + assertEqual(1, Math.sign(Infinity)); + assert(Number.isNaN(Math.sign(NaN))); + }}); + + it("String endsWith", function () { with (this) { + assert("12345".endsWith("12345")); + assert("12345".endsWith("45")); + assert("12345".endsWith("5")); + assert("12345".endsWith("")); + assertNot("12345".endsWith("0")); + assertNot("12345".endsWith("35")); + assertNot("12345".endsWith("34")); + }}); + + it("String startsWith", function () { with (this) { + assert("12345".startsWith("12345")); + assert("12345".startsWith("12")); + assert("12345".startsWith("1")); + assert("12345".startsWith("")); + assertNot("12345".startsWith("0")); + assertNot("12345".startsWith("13")); + assertNot("12345".startsWith("23")); + }}); + + it("splits path extensions", function () { with (this) { + assertEqual(["a", ".png"], yuu.splitPathExtension("a.png")); + assertEqual(["a.b", ".png"], yuu.splitPathExtension("a.b.png")); + assertEqual(["a", ""], yuu.splitPathExtension("a")); + assertEqual(["a.b/c", ".png"], yuu.splitPathExtension("a.b/c.png")); + assertEqual([".gitignore", ""], yuu.splitPathExtension(".gitignore")); + }}); + + JS.Test.describe("resourcePath", function () { with (this) { + var f = yuu.resourcePath; + it("leaves ordinary paths alone", function () { with (this) { + assertEqual("foo.png", f("foo.png", "error", "error")); + assertEqual("foo/bar.png", f("foo/bar.png", "error", "error")); + assertEqual("foo@2x.png", f("foo@2x.png", "error", "error")); + }}); + it("expands @ paths", function () { with (this) { + assertEqual("data/images/foo.png", f("@foo", "images", "png")); + assertEqual("data/images/foo.png", f("@foo.png", "images", "png")); + assertEqual("data/images/foo.jpg", f("@foo.jpg", "images", "png")); + assertEqual("data/images/foo/bar.png", f("@foo/bar", "images", "png")); + assertEqual("foo/data/images/bar.png", f("foo/@bar", "images", "png")); + }}); + it("expands yuu/@ paths", function () { with (this) { + assertEqual(yuu.PATH + "data/images/bar.png", + f("yuu/@bar", "images", "png")); + }}); + }}); + + JS.Test.describe('lerping', function () { with (this) { + it("lerps numbers", function () { with (this) { + assertEqual(0.0, (0).lerp(1, 0.0)); + assertEqual(0.5, (0).lerp(1, 0.5)); + assertEqual(1.0, (0).lerp(1, 1.0)); + }}); + + var arrayTypes = { + "untyped": Array, + "float32": Float32Array, + "float64": Float64Array, + "int8": Int8Array, + "uint8": Uint8Array, + "int16": Int16Array, + "uint16": Uint16Array, + "int32": Int32Array, + "uint32": Uint32Array + }; + + Object.keys(arrayTypes).forEach(function (name) { + var A = arrayTypes[name]; + it("lerps " + name + " arrays element-wise", function () { with (this) { + var a = new A([0, 0, 0]); + var b = new A([0, 10, 20]); + assertEqual(a, a.lerp(b, 0)); + assertEqual(b, a.lerp(b, 1)); + assertEqual(new A([0, 5, 10]), a.lerp(b, 0.5)); + }}); + }); + + Object.keys(arrayTypes).forEach(function (name) { + var A = arrayTypes[name]; + it("slices " + name + " arrays", function () { with (this) { + var a = new A([0, 1, 2, 3, 4, 5]); + var b = a.slice(); + assertEqual(a, b); + assertKindOf(A, b); + assertNotSame(a, b); + }}); + }); + }}); +}}); diff --git a/test/spec/yuu/input.js b/test/spec/yuu/input.js new file mode 100644 index 0000000..1bd00e4 --- /dev/null +++ b/test/spec/yuu/input.js @@ -0,0 +1,146 @@ +var JS = this.JS || require('jstest'); +var yuu = require('yuu/input'); + +JS.Test.describe('yuu input', function () { with (this) { + it("has consistent key names", function () { with (this) { + Object.keys(yuu.KEY_NAMES).forEach(function (code) { + assertEqual(yuu.KEY_NAMES[code], yuu.keyCodeName(code)); + }); + }}); + it("handles unknown key names", function () { with (this) { + Object.keys(yuu.KEY_NAMES).forEach(function (name) { + assertEqual("key:123456", yuu.keyCodeName(123456)); + }); + }}); + it("handles event-known key names", function () { with (this) { + Object.keys(yuu.KEY_NAMES).forEach(function (name) { + assertEqual("bogus", yuu.keyCodeName(123456, "BOGUS")); + }); + }}); + it("ignores browser default names", function () { with (this) { + Object.keys(yuu.KEY_NAMES).forEach(function (name) { + assertEqual("key:123456", yuu.keyCodeName(123456, "Unidentified")); + }); + }}); + + JS.Test.describe('KeyBindSet', function () { with (this) { + it("holds binds", function () { with (this) { + var bindset = new yuu.KeyBindSet(); + assertEqual(0, bindset.binds.length); + bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' }); + assertEqual(2, bindset.binds.length); + }}); + + it("binds new binds", function () { with (this) { + var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' }); + assertEqual(2, bindset.binds.length); + bindset.bind('c', 'c'); + assertEqual(3, bindset.binds.length); + }}); + + it("doesn't bind duplicates", function () { with (this) { + var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' }); + assertEqual(2, bindset.binds.length); + bindset.bind("a", "a different a"); + assertEqual(2, bindset.binds.length); + bindset.bind("a+b", "a and b"); + assertEqual(3, bindset.binds.length); + bindset.bind("b+a", "b and a"); + assertEqual(3, bindset.binds.length); + }}); + + it("unbinds", function () { with (this) { + var bindset = new yuu.KeyBindSet({ a: 'a', b: 'b' }); + assertEqual(2, bindset.binds.length); + bindset.unbind('a'); + assertEqual(1, bindset.binds.length); + bindset.unbind('x'); + assertEqual(1, bindset.binds.length); + bindset.bind('a', 'a'); + assertEqual(2, bindset.binds.length); + }}); + }}); + + JS.Test.describe('InputState', function () { with (this) { + var bindset1 = new yuu.KeyBindSet({ + "a": "a", + "b": "++b", + "c": "+c", + "a+b": "a and b", + }); + var bindset2 = new yuu.KeyBindSet({ + "a": "a different a", + "a+b+c": "a and b and c", + "mousemove": "mouse moved", + "d+mousemove": "mouse moved and d", + }); + + it("activates simple binds", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(["a"], input.keydown("a")); + assertNot(input.keyup("a")); + assertEqual(["a"], input.keydown("a")); + assertNot(input.keyup("a")); + }}); + + it("doesn't activate nothing", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(null, input.keydown("x")); + assertEqual(null, input.keyup("x")); + }}); + + it("doesn't repeat", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(["a"], input.keydown("a")); + assertEqual([], input.keydown("a")); + input.keyup("a"); + assertEqual(["a"], input.keydown("a")); + }}) + + it("activates toggle binds", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(["++b"], input.keydown("b")); + }}); + + it("activates flag binds", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(["+c"], input.keydown("c")); + assertEqual([], input.keydown("c")); + assertEqual(["-c"], input.keyup("c")); + }}); + + it("handles multiple keys", function () { with (this) { + var input = new yuu.InputState([bindset1]); + assertEqual(["a"], input.keydown("a")); + assertEqual(["a and b"], input.keydown("b")); + }}); + + it("handles change states", function () { with (this) { + var input = new yuu.InputState([bindset2]); + assertEqual(["mouse moved"], input.mousemove()); + assertEqual(["mouse moved"], input.mousemove()); + }}); + + it("handles change states with a key", function () { with (this) { + var input = new yuu.InputState([bindset2]); + assertEqual(null, input.keydown("d")); + assertEqual(["mouse moved and d"], input.mousemove()); + assertEqual(["mouse moved and d"], input.mousemove()); + assertEqual(null, input.keyup("d")); + assertEqual(["mouse moved"], input.mousemove()); + }}); + + it("masks lower sets", function () { with (this) { + var input = new yuu.InputState([bindset1, bindset2]); + assertEqual(["a different a"], input.keydown("a")); + }}); + + it("always picks the longest command", function () { with (this) { + var input = new yuu.InputState([bindset1, bindset2]); + input.keydown("b"); + assertEqual(["a and b"], input.keydown("a")); + assertEqual(["a and b and c"], input.keydown("c")); + }}); + + }}); +}}); diff --git a/test/spec/yuu/storage.js b/test/spec/yuu/storage.js new file mode 100644 index 0000000..3361d5b --- /dev/null +++ b/test/spec/yuu/storage.js @@ -0,0 +1,113 @@ +var JS = this.JS || require('jstest'); +require('yuu/pre'); +var ystorage = require('yuu/storage'); + +JS.Test.describe('ystorage', function () { with (this) { + JS.Test.describe("PrefixedStorage", function () { with (this) { + before(function () { + this.backend = new ystorage.FakeStorage(); + this.a = new ystorage.PrefixedStorage(this.backend, "a"); + this.b = new ystorage.PrefixedStorage(this.backend, "b"); + this.c1 = new ystorage.PrefixedStorage(this.backend, "c"); + this.c2 = new ystorage.PrefixedStorage(this.backend, "c"); + }); + + it('starts empty', function () { with (this) { + assertEqual(0, this.backend.length); + assertEqual(0, this.a.length); + assertEqual(0, this.b.length); + assertEqual(0, this.c1.length); + assertEqual(0, this.c2.length); + assertEqual(null, this.backend.key(0)); + assertEqual(null, this.a.key(0)); + assertEqual(null, this.b.key(0)); + assertEqual(null, this.c1.key(0)); + assertEqual(null, this.c2.key(0)); + }}); + + it('stores and retrieves a value', function () { with (this) { + this.a.setItem("key", "value"); + assertEqual(1, this.backend.length); + assertEqual(1, this.a.length); + assertEqual(0, this.b.length); + assertEqual(0, this.c1.length); + assertEqual(0, this.c2.length); + assertEqual("a -- key", this.backend.key(0)); + assertEqual("key", this.a.key(0)); + assertEqual(null, this.b.key(0)); + assertEqual(null, this.c1.key(0)); + assertEqual(null, this.c2.key(0)); + }}); + + it('avoids other values when clearing', function () { with (this) { + this.a.setItem("key", "value a"); + this.b.setItem("key", "value b"); + assertEqual(2, this.backend.length); + assertEqual(1, this.a.length); + assertEqual(1, this.b.length); + this.a.clear(); + assertEqual(1, this.backend.length); + assertEqual(0, this.a.length); + assertEqual(1, this.b.length); + assertEqual(null, this.a.key(0)); + assertEqual(null, this.a.getItem("key")); + assertEqual("key", this.b.key(0)); + assertEqual("value b", this.b.getItem("key")); + }}); + + it('shares with the same prefix', function () { with (this) { + this.c1.setItem("key", "value"); + assertEqual("key", this.c1.key(0)); + assertEqual("key", this.c2.key(0)); + assertEqual("value", this.c1.getItem("key")); + assertEqual("value", this.c2.getItem("key")); + }}); + + }}); + + JS.Test.describe("PrefixedStorage", function () { with (this) { + it('stores objects', function () { with (this) { + var storage = new ystorage.Storage(); + assertEqual(undefined, storage.getObject("key")); + storage.setObject("key", [1, 2, 3]); + assertEqual([1, 2, 3], storage.getObject("key")); + }}); + it('handles defaults', function () { with (this) { + var storage = new ystorage.Storage(null, { + 'should be null': null, + 'should be undefined': undefined, + 'should be false': false + }); + assertEqual(null, storage.getObject('should be null')); + assertEqual(false, storage.getObject('should be false')); + assertEqual(undefined, storage.getObject('should be undefined')); + assertEqual(undefined, storage.getObject('should not exist')); + }}); + it('handles fallbacks', function () { with (this) { + var storage = new ystorage.Storage(); + assertEqual(undefined, storage.getObject('?', undefined)); + assertEqual(null, storage.getObject('?', null)); + assertEqual(false, storage.getObject('?', false)); + }}); + it('prefers defaults to fallbacks', function () { with (this) { + var storage = new ystorage.Storage(null, { + 'should be null': null, + 'should be undefined': undefined, + 'should be false': false + }); + assertEqual(null, storage.getObject('should be null', 0)); + assertEqual(false, storage.getObject('should be false', 0)); + assertEqual(undefined, storage.getObject('should be undefined', 0)); + assertEqual(0, storage.getObject('should not exist', 0)); + }}); + it('keeps defaults', function () { with (this) { + var storage = new ystorage.Storage(null, { a: 'a' }); + assertEqual('a', storage.getObject('a')); + storage.setObject('a', 'b'); + assertEqual('b', storage.getObject('a')); + storage.removeObject('a'); + assertEqual('a', storage.getObject('a')); + }}); + }}) + +}}); diff --git a/test/spec/yuu/yT.js b/test/spec/yuu/yT.js new file mode 100644 index 0000000..dc07ccb --- /dev/null +++ b/test/spec/yuu/yT.js @@ -0,0 +1,227 @@ +var JS = this.JS || require('jstest'); +var yT = this.yT || require('yuu/yT'); + +JS.Test.describe('yT', function() { with (this) { + + it("makes Object subclasses", function () { with (this) { + var A = yT(Object, {}); + assertKindOf(Object, new A()); + }}); + it("makes Object subclasses from a prototype", function () { with (this) { + var A = yT(Object.prototype, {}); + assertKindOf(Object, new A()); + }}); + it("makes a non-Object subclass", function () { with (this) { + var A = yT(null, {}); + var a = new A(); + assertKindOf("object", a); + assertNot(a instanceof Object); + }}); + it("makes Object subclasses by default", function () { with (this) { + var A = yT({}); + assertKindOf(Object, new A()); + }}); + it("makes nested subclasses", function () { with (this) { + var A = yT({}); + var B = yT(A, {}); + assertKindOf(Object, new B()); + assertKindOf(A, new B()); + assertKindOf(B, new B()); + }}); + it("makes nested subclasses from a prototype", function () { with (this) { + var A = yT({}); + var B = yT(A.prototype, {}); + assertKindOf(Object, new B()); + assertKindOf(A, new B()); + assertKindOf(B, new B()); + }}); + + it("sets constructors", function () { with (this) { + function ctor () {}; + var A = yT({ constructor: ctor }); + var a = new A(); + assertSame(A, a.constructor); + assertSame(ctor, a.constructor); + }}); + it("auto-creates root constructors", function () { with (this) { + var A = yT({}); + var a = new A(); + assertSame(A, a.constructor); + }}); + it("auto-creates parent-calling constructors", function () { with (this) { + var A = yT({ constructor: function (x, y) { this.i = x + y; } }); + var B = yT(A, {}); + var b = new B(1, 2); + assertEqual(3, b.i); + assertSame(B, b.constructor); + assertNotSame(A, b.constructor); + }}); + + it("creates data descriptors", function () { with (this) { + var A = yT({ X: { value: 1 } }); + var a = new A(); + assertEqual(1, a.X); + a.X = 2; + assertEqual(1, a.X, "X should not be writable"); + }}); + it("creates writable data descriptors", function () { with (this) { + var A = yT({ x: { value: 1, writable: true } }); + var a = new A(); + assertEqual(1, a.x); + a.x = 2; + assertEqual(2, a.x); + }}); + it("creates bare value descriptors", function () { with (this) { + var A = yT({ X: 1 }); + var a = new A(); + assertEqual(1, a.X); + a.X = 2; + assertEqual(1, a.X, "X should not be writable"); + }}); + it("creates read-only aliases", function () { with (this) { + var A = yT({ a: { alias: "b", readonly: true } }); + var a = new A(); + a.b = "hello"; + assertEqual("hello", a.a); + a.b = "world"; + assertEqual("world", a.a); + a.a = "goodbye"; + assertEqual("world", a.a); + assertEqual("world", a.b); + }}); + it("creates read-write aliases", function () { with (this) { + var A = yT({ a: { alias: "b", readonly: false } }); + var a = new A(); + a.b = "hello"; + assertEqual("hello", a.a); + a.b = "world"; + assertEqual("world", a.a); + a.a = "goodbye"; + assertEqual("goodbye", a.a); + assertEqual("goodbye", a.b); + }}); + it("creates read-write aliases by default", function () { with (this) { + var A = yT({ a: { alias: "b" } }); + var a = new A(); + a.b = "hello"; + assertEqual("hello", a.a); + a.b = "world"; + assertEqual("world", a.a); + a.a = "goodbye"; + assertEqual("goodbye", a.a); + assertEqual("goodbye", a.b); + }}); +}}); + +JS.Test.describe('an example Point type', function() { with (this) { + var Point = yT({ + constructor: function (x, y) { + this.x = x || 0; + this.y = y || 0; + }, + + x: { value: 0, chainable: true }, + y: { value: 0, chainable: true }, + 0: { alias: "x" }, + 1: { alias: "y" }, + xy: { swizzle: "xy" }, + yx: { swizzle: "yx" }, + + angle: { + chainable: true, + get: function () { + return Math.atan2(this.y, this.x); + }, + set: function (angle) { + var magnitude = this.magnitude; + this.x = Math.cos(angle) * magnitude; + this.y = Math.sin(angle) * magnitude; + } + }, + + magnitude: { + chainable: true, + get: function () { + return Math.sqrt(this.x * this.x + this.y * this.y); + }, + set: function (magnitude) { + var angle = this.angle; + this.x = Math.cos(angle) * magnitude; + this.y = Math.sin(angle) * magnitude; + } + }, + + length: 2 + }); + + it("constructs 0, 0 by default", function () { with (this) { + var p = new Point(); + assertEqual(0, p.x); + assertEqual(0, p.y); + assertEqual(2, p.length); + }}); + + + it("has chainable setters", function () { with (this) { + var p = new Point().setX(1).setY(2); + assertEqual(1, p.x); + assertEqual(2, p.y); + }}); + + it("x is also 0", function () { with (this) { + var p = new Point(); + assertEqual(0, p.x); + assertEqual(0, p[0]); + p.x = 1; + assertEqual(1, p.x); + assertEqual(1, p[0]); + p[0] = 2; + assertEqual(2, p.x); + assertEqual(2, p[0]); + }}); + it("y is also 1", function () { with (this) { + var p = new Point(); + assertEqual(0, p.y); + assertEqual(0, p[1]); + p.y = 1; + assertEqual(1, p.y); + assertEqual(1, p[1]); + p[1] = 2; + assertEqual(2, p.y); + assertEqual(2, p[1]); + }}); + + it("x and y have swizzling", function () { with (this) { + var p = new Point(); + assertEqual([0, 0], p.xy); + assertEqual([0, 0], p.yx); + p.xy = [1, 0]; + assertEqual([1, 0], p.xy); + assertEqual([0, 1], p.yx); + p.xy = p.yx; + assertEqual([0, 1], p.xy); + assertEqual([1, 0], p.yx); + }}); + + it("can change magnitude", function () { with (this) { + var p = new Point(1, 0); + assertEqual(1, p.magnitude); + p.magnitude = 10; + assertEqual([10, 0], p.xy); + }}); + it("can change angle", function () { with (this) { + var delta = 1e-13; + var p = new Point(1, 0); + assertEqual(0, p.angle); + p.angle = Math.PI / 2; + assertInDelta(Math.PI / 2, p.angle, delta); + assertInDelta(0, p.x, delta); + assertInDelta(1, p.y, delta); + p.angle = Math.PI / 4; + assertInDelta(Math.PI / 4, p.angle, delta); + assertInDelta(Math.sqrt(2) / 2, p.x, delta); + assertInDelta(Math.sqrt(2) / 2, p.y, delta); + assertInDelta(p.x, p.y, delta); + }}); +}}); + diff --git a/test/spec/yuu/yf.js b/test/spec/yuu/yf.js new file mode 100644 index 0000000..5205aad --- /dev/null +++ b/test/spec/yuu/yf.js @@ -0,0 +1,227 @@ +var JS = this.JS || require('jstest'); +require('yuu/pre'); +var yf = this.yf || require('yuu/yf'); + +JS.Test.describe('yf', function() { with (this) { + + it("allKeys", function () { with (this) { + assert(yf.contains(yf.allKeys(Object(1)), "hasOwnProperty")); + assertNot(yf.contains(yf.allKeys(Object(1)), "not a property")); + }}); + + it('argcd', function () { with (this) { + var f = yf.argcd( + function () { return 0; }, + function (a) { return 1; }, + function (a, b, c, d) { return 4; } + ); + assertEqual(0, f()); + assertEqual(1, f("")); + assertEqual(4, f("", "", "", "")); + assertThrows(TypeError, f.bind(null, "", "")); + }}); + + it('arrayify', function () { with (this) { + assertEqual([1], yf.arrayify(1)); + assertEqual([1], yf.arrayify([1])); + }}); + + it('clamp', function () { with (this) { + assertEqual(0.5, yf.clamp(0.5, 0, 1)); + assertEqual(1, yf.clamp(10, 0, 1)); + assertEqual(0, yf.clamp(-1, 0, 1)); + }}) + + it('compose', function () { with (this) { + function inc (a) { return a + 1; } + function add (a, b) { return a + b; } + + var plus1 = yf.compose(inc); + var plus2 = yf.compose(inc, inc); + var plus3 = yf.compose(inc, inc, inc); + var addplus3 = yf.compose(plus3, add); + + assertEqual(1, plus1(0)); + assertEqual(2, plus2(0)); + assertEqual(3, plus3(0)); + assertEqual(6, addplus3(1, 2)); + + function this_ () { return this; } + var f = yf.compose(this_, this_, this_); + assertSame(this, f.call(this)); + }}); + + it('construct', function () { with (this) { + assertEqual([], yf.construct(Array, [])); + assertEqual([1, 2, 3], yf.construct(Array, [1, 2, 3])); + }}); + + it('counter', function () { with (this) { + var c0 = yf.counter(0); + var c5 = yf.counter(5); + assertEqual(0, c0()); + assertEqual(1, c0()); + assertEqual(2, c0()); + assertEqual(5, c5()); + assertEqual(6, c5()); + }}); + + it('new_', function () { with (this) { + var a = yf.new_(Array); + assertEqual([], a()); + assertEqual([1, 2, 3], a(1, 2, 3)); + }}); + + it('foldl', function () { with (this) { + function sumString (v, i) { return v + i.toString(); } + var a = [1, 2, 3]; + assertEqual("123", yf.foldl(sumString, a)); + assertEqual("0123", yf.foldl(sumString, a, "0")); + }}); + + it('foldr', function () { with (this) { + function sumString (i, v) { return v + i.toString(); } + var a = [1, 2, 3]; + assertEqual("321", yf.foldr(sumString, a)); + assertEqual("4321", yf.foldr(sumString, a, "4")); + }}); + + JS.Test.describe('isFunction', function() { with (this) { + it("detects functions", function () { with (this) { + assert(yf.isFunction(Object)); + assert(yf.isFunction(assert)); + assert(yf.isFunction(yf.isFunction)); + }}); + it("detects non-functions", function () { with (this) { + assertNot(yf.isFunction(undefined)); + assertNot(yf.isFunction(null)); + assertNot(yf.isFunction(23)); + assertNot(yf.isFunction(yf)); + }}); + }}); + + JS.Test.describe('isString', function () { with (this) { + it("detects strings", function () { with (this) { + assert(yf.isString("")); + assert(yf.isString("hello world")); + }}); + it("detects Strings", function () { with (this) { + assert(yf.isString(new String(""))); + assert(yf.isString(new String("hello world"))); + }}); + it("detects non-strings", function () { with (this) { + assertNot(yf.isString(undefined)); + assertNot(yf.isString(null)); + assertNot(yf.isString(23)); + assertNot(yf.isString([1, 2, 3])); + }}); + }}); + + JS.Test.describe('insertBefore', function () { with (this) { + it("inserts at start", function () { with (this) { + var a = [1, 2]; + yf.insertBefore(a, 0, 1); + assertEqual([0, 1, 2], a); + }}); + it("inserts at middle", function () { with (this) { + var a = [0, 2]; + yf.insertBefore(a, 1, 2); + assertEqual([0, 1, 2], a); + }}); + it("inserts at end if missing", function () { with (this) { + var a = [0, 1]; + yf.insertBefore(a, 2, 99); + assertEqual([0, 1, 2], a); + }}); + }}); + + JS.Test.describe('contains', function () { with (this) { + var a = [1, 2, 3]; + it("finds things", function () { with (this) { + assert(yf.contains(a, 1)); + assert(yf.contains(a, 3)); + }}); + it("doesn't find things", function () { with (this) { + assertNot(yf.contains(a, -1)); + assertNot(yf.contains(a, 4)); + }}); + it("handles the empty sequence", function () { with (this) { + assertNot(yf.contains([], 1)); + assertNot(yf.contains([], undefined)); + assertNot(yf.contains([], null)); + }}); + it("uses identity, not equality", function () { with (this) { + assertNot(yf.contains(a, "1")); + }}); + }}); + + JS.Test.describe('repeat', function () { with (this) { + it("repeats nothing", function () { with (this) { + assertEqual([], yf.repeat("hello", 0)); + assertEqual([], yf.repeat(1, 0)); + assertEqual([], yf.repeat(null, 0)); + }}); + it("repeats something", function () { with (this) { + assertEqual([1], yf.repeat(1, 1)); + assertEqual([1, 1], yf.repeat(1, 2)); + assertEqual([1, 1, 1, 1], yf.repeat(1, 4)); + }}); + }}) + + JS.Test.describe('without', function () { with (this) { + var a = [1, 2, 3]; + it("doesn't remove wrongly", function () { with (this) { + assertEqual([1, 2, 3], yf.without(a, 4)); + }}); + it("removes by ===", function () { with (this) { + assertEqual([1, 2, 3], yf.without(a, "1")); + }}); + it("removes one element", function () { with (this) { + assertEqual([2, 3], yf.without(a, 1)); + }}); + it("removes multiple elements", function () { with (this) { + assertEqual([3], yf.without(a, 1, 2)); + assertEqual([2], yf.without(a, 1, 3)); + assertEqual([], yf.without(a, 1, 2, 3)); + }}); + it("handles the empty sequence", function () { with (this) { + assertEqual([], yf.without([])); + assertEqual([], yf.without([], 1)); + assertEqual([], yf.without([], 1, 2)); + }}); + }}); + + it("some", function () { with (this) { + assert(yf.some(null, [1])); + assert(yf.some(null, [1, 2])); + assert(yf.some(null, [1, 0])); + + assertNot(yf.some(null, [])); + assertNot(yf.some(null, [0, NaN])); + assertNot(yf.some(null, [0])); + }}); + + it('every', function () { with (this) { + assert(yf.every(null, [])); + assert(yf.every(null, [1])); + assert(yf.every(null, [1, 2])); + + assertNot(yf.every(null, [0])); + assertNot(yf.every(null, [1, 0])); + assertNot(yf.every(null, [1, 2, NaN])); + }}); + + it('none', function () { with (this) { + assert(yf.none(null, [])); + assert(yf.none(null, [0])); + assert(yf.none(null, [0, NaN])); + + assertNot(yf.none(null, [1])); + assertNot(yf.none(null, [1, 0])); + }}); + + it('pluck', function () { with (this) { + assertEqual([], yf.pluck("foo", [])); + assertEqual([1, 2, 3], yf.pluck("length", ["a", "ab", "abc"])); + }}); +}}); diff --git a/tools/LICENSE.rcedit b/tools/LICENSE.rcedit new file mode 100644 index 0000000..437dd58 --- /dev/null +++ b/tools/LICENSE.rcedit @@ -0,0 +1,57 @@ +Copyright (c) 2013 GitHub Inc. +https://github.com/atom/rcedit/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Also it's mostly using code from Rescle that GitHub has stripped the +copyright notice from, because who needs to follow licenses when +you've got piles of VC money and a terrible culture literally woven +into your furniture? + +-- + +Copyright (c) 2009 Yoshio Okumura +https://code.google.com/p/rescle/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tools/composer/composer.js b/tools/composer/composer.js new file mode 100644 index 0000000..75bb81c --- /dev/null +++ b/tools/composer/composer.js @@ -0,0 +1,85 @@ +"use strict"; + +function titleCase (text) { + return text.replace("_", " ").replace(/\w\S*/g, function (word) { + return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); + }); +} + +function setOptions (select, dict) { + yf.irange.call(select, select.remove, select.options.length - 1, -1, -1); + yf.each(function (name) { + var opt = document.createElement("option"); + opt.text = titleCase(name); + opt.id = name; + select.appendChild(opt); + }, Object.keys(dict).sort()); +} + +window.addEventListener("load", function () { + var playing; + + yuu.init({}).then(function () { + setOptions(document.getElementById("instrument"), yuu.Instruments); + setOptions(document.getElementById("scale"), yuu.Scales); + }); + + function play () { + if (playing) + playing.disconnect(); + var instruments = document.getElementById("instrument"); + var scales = document.getElementById("scale"); + var instrument = yuu.Instruments[ + instruments.options[instruments.selectedIndex].id]; + var scale = yuu.Scales[ + scales.options[scales.selectedIndex].id]; + var score = yuu.parseScore( + document.getElementById("score").value, + scale, document.getElementById("tonic").value); + var bps = +document.getElementById("tempo").value / 60; + playing = yuu.audio.createGain(); + playing.gain.value = 0.5; + var t = yuu.audio.currentTime + 0.25; + yf.each(function (note) { + instrument.createSound( + yuu.audio, + t + note.time / bps, + note.hz, + 1.0, + note.duration / bps + ).connect(playing); + }, score); + playing.connect(yuu.audio.destination); + } + + function jam (key) { + if (key === "`") + key = "0"; + else if (key === "0") + key = "10"; + var instruments = document.getElementById("instrument"); + var scales = document.getElementById("scale"); + var tonic = document.getElementById("tonic").value; + var instrument = yuu.Instruments[ + instruments.options[instruments.selectedIndex].id]; + var scale = yuu.Scales[scales.options[scales.selectedIndex].id]; + var note = yuu.parseScore(key, scale, tonic)[0]; + var bps = +document.getElementById("tempo").value / 60; + if (note) + instrument.createSound( + yuu.audio, + yuu.audio.currentTime + 0.01, + note.hz, + 1.0, + note.duration / bps + ).connect(yuu.audio.destination); + } + + document.getElementById("play").addEventListener("click", play); + + document.getElementById("jam") + .addEventListener("keydown", function (event) { + jam(yuu.keyEventName(event)); + }); +}); + diff --git a/tools/composer/index.html b/tools/composer/index.html new file mode 100644 index 0000000..3202fab --- /dev/null +++ b/tools/composer/index.html @@ -0,0 +1,35 @@ + + + + Yuu Composer + + + + + + + + + + + +
+ click and type ` to 0 to jam +
+
+ +
+ +
+
+ Instrument:
+ Tonic:
+ Scale:
+ Tempo (BPM):
+
+ + diff --git a/tools/generate-appcache b/tools/generate-appcache new file mode 100755 index 0000000..a729f0b --- /dev/null +++ b/tools/generate-appcache @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +# Turn a website into an HTML5 application with an application cache +# manifest. +# +# http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html +# https://developer.mozilla.org/en/docs/HTML/Using_the_application_cache + +import os +import re +import shutil +import time + +def is_html(filename): + return filename.lower().endswith(".html") + +def main(appdir): + if not os.path.isdir(appdir): + raise StandardError("input (%r) is not a directory" % appdir) + all_files = [] + for root, dirnames, filenames in os.walk(appdir): + root = os.path.relpath(root, appdir) + for filename in filenames: + all_files.append(os.path.join(root, filename)) + all_files.sort() + appcache = os.path.join(appdir, "manifest.appcache") + with open(appcache, "w") as fobj: + fobj.write("CACHE MANIFEST\n") + fobj.write("# Generated on %s\n" % time.strftime("%Y %T %z")) + for filename in all_files: + fobj.write(filename + "\n") + + for filename in filter(is_html, all_files): + filename = os.path.join(appdir, filename) + # This call to relpath is the entire reason this tool is in + # Python and not a shell script. + relpath = os.path.relpath(appcache, os.path.dirname(filename)) + html = open(filename).read() + html = re.sub(" ])", '"']))+.)["']?""" + +def main(appdir, version=None): + if not os.path.isdir(appdir): + raise StandardError("input (%r) is not a directory" % appdir) + indexes = [] + icons = [] + for root, dirnames, filenames in os.walk(appdir): + root = os.path.relpath(root, appdir) + for filename in filenames: + if filename.lower() == "index.html": + indexes.append(os.path.join(root, filename)) + if ("icon" in filename.lower() + and filename.lower().endswith((".ico", ".png"))): + icons.append(os.path.join(root, filename)) + + indexes.sort(key=lambda fn: (fn.count("/"), fn)) + icons.sort(key=lambda fn: (fn.count("/"), fn)) + name = os.path.basename(appdir).split(".")[0] + package = { + "main": indexes[0], + "name": name, + "version": version or "0.0.0", + "chromium-args": "--enable-webgl --ignore-gpu-blacklist", + "window": { + "show": False, + "toolbar": False, + "title": name, + "min_width": 300, + "min_height": 200, + } + } + + if icons: + package["window"]["icon"] = icons[0] + + with open(os.path.join(appdir, indexes[0]), "r") as fobj: + header = fobj.read(4096) + try: + title = re.search("([^<]+)<", header).groups()[0] + except AttributeError: + print >>sys.stderr, "No <title> found in %r." % fobj.name + else: + package["window"]["title"] = title + try: + version = re.search(attr("version"), header).groups()[0] + except AttributeError: + print >>sys.stderr, "No version found in %r." % fobj.name + else: + package["version"] = version + try: + appid = re.search(attr("appid"), header).groups()[0] + except AttributeError: + print >>sys.stderr, "No appid found in %r." % fobj.name + else: + package["name"] = appid + + if "+" not in package["version"]: + package["version"] += "+" + package["version"] += time.strftime("%Y%m%d%H%M%S") + + with open(os.path.join(appdir, "package.json"), "w") as fobj: + json.dump(package, fobj) + +if __name__ == "__main__": + import sys + main(*sys.argv[1:]) diff --git a/tools/generate-osx-app b/tools/generate-osx-app new file mode 100755 index 0000000..dd05cdd --- /dev/null +++ b/tools/generate-osx-app @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +import os +import zipfile +import re +import plistlib +import shutil +import json +import re + +from os.path import join, basename + +def xp_filename(basename): + return re.sub('["<>*?|\\\\]', "_", + basename.replace("/", "-").replace(":", ".")) + +def versionify(version): + return ".".join(filter(lambda x: x.isdigit(), + re.split("[-+.]", version))[:3]) + +def main(nwdir, nwpackage): + if not os.path.isdir(nwdir): + raise StandardError("input (%r) is not a directory" % nwdir) + nwzip = zipfile.ZipFile(nwpackage) + icnss = filter(lambda f: f.lower().endswith(".icns"), + nwzip.namelist()) + package = json.load(nwzip.open("package.json")) + app = join(nwdir, "node-webkit.app") + title = package["window"]["title"] + exe = package["name"].split(".")[-1] + plist = dict( + CFBundleDisplayName=title, + CFBundleExecutable=exe, + CFBundleIdentifier=package["name"], + CFBundleInfoDictionaryVersion="6.0", + CFBundleName=title, + CFBundlePackageType="APPL", + CFBundleShortVersionString=package["version"], + CFBundleIconFile="nw.icns", + CFBundleVersion=versionify(package["version"]), + LSMinimumSystemVersion="10.6.0", + NSPrincipalClass="NSApplication", + NSSupportsAutomaticGraphicsSwitching=True, + ) + if icnss: + icnss.sort() + os.remove(join(app, "Contents", "Resources", "nw.icns")) + nwzip.extract(icnss[0], join(app, "Contents", "Resources")) + plist["CFBundleIconFile"] = icnss[0] + plistlib.writePlist(plist, join(app, "Contents/Info.plist")) + exedir = join(app, "Contents", "MacOS") + shutil.move(join(exedir, "node-webkit"), join(exedir, exe)) + shutil.copy(nwpackage, join(app, "Contents", "Resources", "app.nw")) + shutil.move(app, join(app, "..", xp_filename(title) + ".app")) + +if __name__ == "__main__": + import sys + main(*sys.argv[1:]) + diff --git a/tools/nw-linux-wrapper b/tools/nw-linux-wrapper new file mode 100755 index 0000000..e195611 --- /dev/null +++ b/tools/nw-linux-wrapper @@ -0,0 +1,26 @@ +#!/bin/sh + +# https://github.com/rogerwang/node-webkit/issues/136 +# https://github.com/rogerwang/node-webkit/wiki/The-solution-of-lacking-libudev.so.0 +# +# Though that page would be more accurately titled "apparently even +# Intel, Google, and every commercial distribution can't be bothered +# to maintain trivial compatibility on desktop Linux." + +SELF=$(readlink -f "$0") +NWDIR=$(dirname "$SELF")/nw +NWBIN=$(command -v "$NWDIR/nw" nw | head -n 1) +NWFLAGS="--enable-webgl --ignore-gpu-blacklist" +PACKAGE="$NWDIR/package.nw" + +if [ ! -x "$NWBIN" ]; then + echo "node-webkit (nw) executable could not be found." >&2 + exit 127 +fi + +"$NWBIN" $NWFLAGS "$@" "$PACKAGE" +if [ "$?" = "127" -a -w "$NWBIN" ]; then + echo "Applying libudev.so.1 hack." >&2 + sed -i 's/udev\.so\.0/udev.so.1/g' "$NWBIN" + "$NWBIN" $NWFLAGS "$@" "$PACKAGE" +fi diff --git a/tools/rcedit.exe b/tools/rcedit.exe new file mode 100644 index 0000000..9a8cb71 Binary files /dev/null and b/tools/rcedit.exe differ