From: Joe Wreschnig Date: Thu, 4 Sep 2014 19:48:29 +0000 (+0200) Subject: Initial import. X-Git-Url: https://git.yukkurigames.com/?p=featherfall2.git;a=commitdiff_plain;h=d2962105772fbdc548118bd65cd5bb4934e66085 Initial import. --- d2962105772fbdc548118bd65cd5bb4934e66085 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..06f7e3f --- /dev/null +++ b/Makefile @@ -0,0 +1,102 @@ +#!/usr/bin/make -f + +node-webkit-version := 0.10.2 +node-webkit-version-win-ia32.zip := 0.8.6 + +.DELETE_ON_ERROR: +include rules/programs.mk +include rules/javascript.mk +include rules/icons.mk +include rules/git.mk +include rules/node-webkit.mk +include rules/pngcrush.mk + +.PHONY: all check distclean lint test dist clean serve + +APPLICATION := pwl6 +DISTDIR := build/dist +VERSION := $(call git-describe) +DISTROOT := $(DISTDIR)/$(APPLICATION)-$(VERSION) + +FIND_FILES := -type f ! -name '.*' +FIND_JS := -type f -name '*.js' + +SOURCES := $(shell cd src && find . $(FIND_FILES)) +MY_SOURCES := $(shell cd src && find . $(FIND_JS) -not -path './ext/*') + +TEST_SOURCES := $(shell find test/spec $(FIND_JS)) + +LINT_TARGETS := $(call jshint-stampify,$(MY_SOURCES)) +TEST_TARGETS := $(call jstest-stampify,$(TEST_SOURCES)) + +JSHINTCONFIG := test/jshint.config +JSTEST_NODE_PATH := src +VPATH := src + +ICONSETS := $(shell find src -type d -name '*.iconset') +ICONS := $(ICONSETS:.iconset=.icns) $(ICONSETS:.iconset=.ico) + +IMAGEGZSRC := $(shell find src -type f -name '*.xcf.gz') +IMAGESRC := $(shell find src -type f -name '*.xcf') +IMAGES := $(IMAGEGZSRC:.xcf.gz=.png) $(IMAGESRC:.xcf=.png) + +BUILT := $(ICONS) $(IMAGES) + +HTTP_SERVER_PORT ?= 8000 + +all: check $(BUILT) $(call pngcrush-stampify,$(IMAGES)) + +$(DISTDIR): + mkdir -p $@ + +dist: $(addprefix $(DISTROOT),-src.zip -src.tar.gz .appcache .nw -osx-ia32.zip -osx-x64.zip -linux-ia32.tar.gz -linux-x64.tar.gz -win-ia32.zip) + +test/spec/%.js: %.js + touch $@ + +lint: $(LINT_TARGETS) + +test: $(TEST_TARGETS) + +check: lint test + +serve: | $(npmbindir)/http-server + $(npmbindir)/http-server $(@D) -p $(HTTP_SERVER_PORT) -c-1 + +clean: + $(RM) $(IMAGES) + $(RM) $(ICONS) + $(RM) -r build + +distclean: clean + $(RM) -r node_modules + $(RM) $(node-webkit-archives) + +$(DISTROOT)-src.zip $(DISTROOT)-src.tar.gz: | .git + mkdir -p $(@D) + $(call git-archive,$@,$(notdir $(DISTROOT))/) + +$(DISTROOT).bare.zip: | .git + $(RM) $@ + $(RM) -r $@.tmp + mkdir -p $@.tmp + cd src && $(GIT) archive $(call git-describe) . | tar -x -C ../$@.tmp + $(MAKE) $(BUILT:src/%=$@.tmp/%) + $(RM) $(IMAGESRC:src/%=$@.tmp/%) $(IMAGEGZSRC:src/%=$@.tmp/%) + cd $@.tmp && $(ZIP) ../$(@F) -r . + $(RM) -r $@.tmp + +%.appcache: %.bare.zip tools/generate-appcache + $(RM) -r $@.tmp $@ + $(UNZIP) -d $@.tmp $< + tools/generate-appcache $@.tmp + mv $@.tmp $@ + +# Python's zipfile module generates zipfiles that node-webkit cannot +# read, so delegate to a real zip tool. +%.nw: %.bare.zip tools/generate-nw + $(RM) -r $@ $@.tmp + $(UNZIP) -d $@.tmp $< + tools/generate-nw $@.tmp + cd $@.tmp && $(ZIP) -r ../$(@F) . + $(RM) -r $@.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..c949cec --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Yuu - a WebGL game library + +Software developers do their best now and are preparing. Please wait +warmly until it is ready. + +* * * + +Here are some reasons to avoid this library for now: + +* There's no real documentation. There's no API stability promises. +* No one has actually made a game in it. +* There's no draw batching. Each quad is its own draw call. +* In fact, there's been no optimization in general. +* Before I wrote this I'd never once used the JavaScript `new` keyword. diff --git a/rules/git.mk b/rules/git.mk new file mode 100644 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..8aa0d0b --- /dev/null +++ b/rules/node-webkit.mk @@ -0,0 +1,91 @@ +# This is free and unencumbered software released into the public +# domain. To the extent possible under law, the author of this file +# waives all copyright and related or neighboring rights to it. + +.DELETE_ON_ERROR: + +include $(dir $(realpath $(lastword $(MAKEFILE_LIST))))programs.mk + +node-webkit-platforms := \ + osx-x64.zip osx-ia32.zip \ + linux-x64.tar.gz linux-ia32.tar.gz \ + win-ia32.zip + +node-webkit-pattern := \ + $(addprefix node-webkit-v%-,$(node-webkit-platforms)) + +# These are like 30MB, don't download them every time. +.PRECIOUS: $(node-webkit-pattern) + +$(node-webkit-pattern): + mkdir -p $(@D) + wget -O $@ http://dl.node-webkit.org/v$(*F)/$(@F) || ($(RM) $@ && exit 1) + +node-webkit-version ?= 0.10.2 +node-webkit-prefix ?= node-webkit/ + +node-webkit = $(node-webkit-prefix)node-webkit-v$(firstword $(value node-webkit-version-$1) $(node-webkit-version))-$1 + +node-webkit-archives = \ + $(foreach p,$(node-webkit-platforms),$(call node-webkit,$(p))) + +define node-webkit-package-osx + $(RM) $@ + $(RM) -r $(@:.zip=) + $(UNZIP) -d $(@D) $2 + mv $(@D)/$(notdir $(2:.zip=)) $(@:.zip=) + tools/generate-osx-app $(@:.zip=) $1 + $(RM) $(@:.zip=)/nwsnapshot + mv $(@:.zip=)/credits.html $(@:.zip=)/node-webkit\ credits.html + cd $(@D) && $(ZIP) -r $(@F) $(@F:.zip=) + $(RM) -r $(@:.zip=) +endef + +%-osx-ia32.zip: %.nw $(call node-webkit,osx-ia32.zip) + $(call node-webkit-package-osx,$<,$(word 2,$^)) + +%-osx-x64.zip: %.nw $(call node-webkit,osx-x64.zip) + $(call node-webkit-package-osx,$<,$(word 2,$^)) + +define node-webkit-package-linux + $(RM) $@ + $(RM) -r $(@:.tar.gz=) + tar -C $(@D) -xzf $2 + mkdir -p $(@:.tar.gz=) + mv $(@D)/$(notdir $(2:.tar.gz=)) $(@:.tar.gz=)/nw + cp -a $1 $(@:.tar.gz=)/nw/package.nw + cp -a tools/nw-linux-wrapper $(@:.tar.gz=)/`echo $(notdir $1) | sed -E 's/-[^-]+$$//'` + $(RM) $(@:.tar.gz=)/nw/nwsnapshot + mv $(@:.tar.gz=)/nw/credits.html $(@:.tar.gz=)/nw/node-webkit\ credits.html + tar -czf $@ -C $(@D) $(@F:.tar.gz=) + $(RM) -r $(@:.tar.gz=) +endef + +%-linux-ia32.tar.gz: %.nw $(call node-webkit,linux-ia32.tar.gz) + $(call node-webkit-package-linux,$<,$(word 2,$^)) + +%-linux-x64.tar.gz: %.nw $(call node-webkit,linux-x64.tar.gz) + $(call node-webkit-package-linux,$<,$(word 2,$^)) + +WINE ?= wine + +node-webkit-icon = $(shell $(UNZIP) -p $1 package.json | grep -Eo '"[^"]+.ico"' -m 1) + +define node-webkit-package-win + $(RM) $@ + $(RM) -r $(@:.zip=) + if $(UNZIP) -l $2 credits.html > /dev/null; then $(UNZIP) -d $(@D)/$(notdir $(2:.zip=)) $2; else $(UNZIP) -d $(@D) $2; fi + mv $(@D)/$(notdir $(2:.zip=)) $(@:.zip=) + $(RM) $(@:.zip=)/nwsnapshot.exe + $(UNZIP) -p $< $(call node-webkit-icon,$<) > $(@D)/icon.ico + $(WINE) tools/rcedit.exe $(@:.zip=)/nw.exe --set-icon $(@D)/icon.ico + $(RM) $(@D)/icon.ico + mv $(@:.zip=)/credits.html $(@:.zip=)/node-webkit\ credits.html + cp -a $< $(@:.zip=)/package.nw + mv $(@:.zip=)/nw.exe $(@:.zip=)/`echo $(notdir $1) | sed -E 's/-[^-]+$$/.exe/'` + cd $(@D) && $(ZIP) -r $(@F) $(@F:.zip=) + $(RM) -r $(@:.zip=) +endef + +%-win-ia32.zip: %.nw $(call node-webkit,win-ia32.zip) + $(call node-webkit-package-win,$<,$(word 2,$^)) diff --git a/rules/pngcrush.mk b/rules/pngcrush.mk new file mode 100644 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/.gitignore b/src/data/images/.gitignore new file mode 100644 index 0000000..206018c --- /dev/null +++ b/src/data/images/.gitignore @@ -0,0 +1,5 @@ +icons.ico +icons.icns +book.png +circle.png + diff --git a/src/data/images/book.xcf.gz b/src/data/images/book.xcf.gz new file mode 100644 index 0000000..cc06d1f Binary files /dev/null and b/src/data/images/book.xcf.gz differ diff --git a/src/data/images/circle-inner.png b/src/data/images/circle-inner.png new file mode 100644 index 0000000..9069a43 Binary files /dev/null and b/src/data/images/circle-inner.png differ diff --git a/src/data/images/circle-outer-ee.png b/src/data/images/circle-outer-ee.png new file mode 100644 index 0000000..7040514 Binary files /dev/null and b/src/data/images/circle-outer-ee.png differ diff --git a/src/data/images/circle-outer.png b/src/data/images/circle-outer.png new file mode 100644 index 0000000..0c2e236 Binary files /dev/null and b/src/data/images/circle-outer.png differ diff --git a/src/data/images/circle-rim.png b/src/data/images/circle-rim.png new file mode 100644 index 0000000..366d020 Binary files /dev/null and b/src/data/images/circle-rim.png differ diff --git a/src/data/images/circle.xcf.gz b/src/data/images/circle.xcf.gz new file mode 100644 index 0000000..4c2fa51 Binary files /dev/null and b/src/data/images/circle.xcf.gz differ diff --git a/src/data/images/hand.png b/src/data/images/hand.png new file mode 100644 index 0000000..96e07b2 Binary files /dev/null and b/src/data/images/hand.png differ diff --git a/src/data/images/icons.iconset/icon_128x128.png b/src/data/images/icons.iconset/icon_128x128.png new file mode 100644 index 0000000..6c93aa7 Binary files /dev/null and b/src/data/images/icons.iconset/icon_128x128.png differ diff --git a/src/data/images/icons.iconset/icon_128x128@2x.png b/src/data/images/icons.iconset/icon_128x128@2x.png new file mode 100644 index 0000000..08bd681 Binary files /dev/null and b/src/data/images/icons.iconset/icon_128x128@2x.png differ diff --git a/src/data/images/icons.iconset/icon_16x16.png b/src/data/images/icons.iconset/icon_16x16.png new file mode 100644 index 0000000..570b181 Binary files /dev/null and b/src/data/images/icons.iconset/icon_16x16.png differ diff --git a/src/data/images/icons.iconset/icon_256x256.png b/src/data/images/icons.iconset/icon_256x256.png new file mode 100644 index 0000000..08bd681 Binary files /dev/null and b/src/data/images/icons.iconset/icon_256x256.png differ diff --git a/src/data/images/icons.iconset/icon_32x32.png b/src/data/images/icons.iconset/icon_32x32.png new file mode 100644 index 0000000..6813ad9 Binary files /dev/null and b/src/data/images/icons.iconset/icon_32x32.png differ diff --git a/src/data/images/icons.iconset/icon_32x32@2x.png b/src/data/images/icons.iconset/icon_32x32@2x.png new file mode 100644 index 0000000..a8dc504 Binary files /dev/null and b/src/data/images/icons.iconset/icon_32x32@2x.png differ diff --git a/src/data/images/icons.iconset/icon_64x64.png b/src/data/images/icons.iconset/icon_64x64.png new file mode 100644 index 0000000..a8dc504 Binary files /dev/null and b/src/data/images/icons.iconset/icon_64x64.png differ diff --git a/src/data/images/icons.iconset/icon_64x64@2x.png b/src/data/images/icons.iconset/icon_64x64@2x.png new file mode 100644 index 0000000..6c93aa7 Binary files /dev/null and b/src/data/images/icons.iconset/icon_64x64@2x.png differ diff --git a/src/data/images/sigils.png b/src/data/images/sigils.png new file mode 100644 index 0000000..c69871a Binary files /dev/null and b/src/data/images/sigils.png differ diff --git a/src/data/license.txt b/src/data/license.txt new file mode 100644 index 0000000..066eda5 --- /dev/null +++ b/src/data/license.txt @@ -0,0 +1,26 @@ +Description : Array and textureless GLSL 2D/3D/4D simplex + noise functions. + Author : Ian McEwan, Ashima Arts. + Maintainer : ijm + Lastmod : 20110822 (ijm) + License : Copyright (C) 2011 Ashima Arts. All rights reserved. + Distributed under the MIT License. See LICENSE file. + https:github.com/ashima/webgl-noise + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/data/shaders/noise.glsl b/src/data/shaders/noise.glsl new file mode 100644 index 0000000..84944be --- /dev/null +++ b/src/data/shaders/noise.glsl @@ -0,0 +1,180 @@ +// +// Description : Array and textureless GLSL 2D/3D/4D simplex +// noise functions. +// Author : Ian McEwan, Ashima Arts. +// Maintainer : ijm +// Lastmod : 20110822 (ijm) +// License : Copyright (C) 2011 Ashima Arts. All rights reserved. +// Distributed under the MIT License. See LICENSE file. +// https://github.com/ashima/webgl-noise +// +/* + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +precision highp float; + +vec2 mod289(vec2 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec3 permute(vec3 x) { + return mod289(((x*34.0)+1.0)*x); +} + +vec4 permute(vec4 x) { + return mod289(((x*34.0)+1.0)*x); +} + +vec4 taylorInvSqrt(vec4 r) +{ + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) + { + const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + +// First corner + vec3 i = floor(v + dot(v, C.yyy) ); + vec3 x0 = v - i + dot(i, C.xxx) ; + +// Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min( g.xyz, l.zxy ); + vec3 i2 = max( g.xyz, l.zxy ); + + // x0 = x0 - 0.0 + 0.0 * C.xxx; + // x1 = x0 - i1 + 1.0 * C.xxx; + // x2 = x0 - i2 + 2.0 * C.xxx; + // x3 = x0 - 1.0 + 3.0 * C.xxx; + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y + vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y + +// Permutations + i = mod289(i); + vec4 p = permute( permute( permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); + +// Gradients: 7x7 points over a square, mapped onto an octahedron. +// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) + float n_ = 0.142857142857; // 1.0/7.0 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) + + vec4 x = x_ *ns.x + ns.yyyy; + vec4 y = y_ *ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4( x.xy, y.xy ); + vec4 b1 = vec4( x.zw, y.zw ); + + //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; + //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; + vec4 s0 = floor(b0)*2.0 + 1.0; + vec4 s1 = floor(b1)*2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; + vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; + + vec3 p0 = vec3(a0.xy,h.x); + vec3 p1 = vec3(a0.zw,h.y); + vec3 p2 = vec3(a1.xy,h.z); + vec3 p3 = vec3(a1.zw,h.w); + +//Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + +// Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); + m = m * m; + return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), + dot(p2,x2), dot(p3,x3) ) ); + } + +float snoise(vec2 v) + { + const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 + 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) + -0.577350269189626, // -1.0 + 2.0 * C.x + 0.024390243902439); // 1.0 / 41.0 +// First corner + vec2 i = floor(v + dot(v, C.yy) ); + vec2 x0 = v - i + dot(i, C.xx); + +// Other corners + vec2 i1; + //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0 + //i1.y = 1.0 - i1.x; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + // x0 = x0 - 0.0 + 0.0 * C.xx ; + // x1 = x0 - i1 + 1.0 * C.xx ; + // x2 = x0 - 1.0 + 2.0 * C.xx ; + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + +// Permutations + i = mod289(i); // Avoid truncation effects in permutation + vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) ++ i.x + vec3(0.0, i1.x, 1.0 )); + + vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); + m = m*m ; + m = m*m ; + +// Gradients: 41 points uniformly over a line, mapped onto a diamond. +// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) + + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + +// Normalise gradients implicitly by scaling m +// Approximation of: m *= inversesqrt( a0*a0 + h*h ); + m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); + +// Compute final noise value at P + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} diff --git a/src/data/shaders/noisyblocks.frag b/src/data/shaders/noisyblocks.frag new file mode 100644 index 0000000..be6e051 --- /dev/null +++ b/src/data/shaders/noisyblocks.frag @@ -0,0 +1,23 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +precision highp float; + +varying vec2 fTexCoord; +varying vec4 fColor; +uniform sampler2D tex; + +uniform vec2 resolution; +uniform float cut; +uniform float range; + +void main(void) { + vec2 coord = floor(fTexCoord * resolution); + vec3 n = vec3(coord.xy, cut); + float p = 1.0 - range * abs(snoise(n)); + vec3 modulated = fColor.rgb * p; + vec4 texColor = texture2D(tex, fTexCoord); + gl_FragColor = vec4(modulated * fColor.a, fColor.a) * texColor; +} diff --git a/src/data/shaders/noisyquads.vert b/src/data/shaders/noisyquads.vert new file mode 100644 index 0000000..8388349 --- /dev/null +++ b/src/data/shaders/noisyquads.vert @@ -0,0 +1,26 @@ +/* This is free and unencumbered software released into the public + domain. To the extent possible under law, the author of this file + waives all copyright and related or neighboring rights to it. +*/ + +precision mediump float; + +attribute vec3 position; +attribute vec2 texCoord; +attribute vec4 color; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +uniform float cut; +uniform float range; + +varying vec4 fColor; + +void main(void) { + gl_Position = projection * view * model * vec4(position, 1.0); + vec2 n = vec2(texCoord.x, cut); + float p = 1.0 - range * abs(snoise(n)); + fColor = vec4(p * color.rgb, color.a); +} diff --git a/src/data/sound/book-appear.wav b/src/data/sound/book-appear.wav new file mode 100644 index 0000000..bc86223 Binary files /dev/null and b/src/data/sound/book-appear.wav differ diff --git a/src/data/sound/book-dismiss.wav b/src/data/sound/book-dismiss.wav new file mode 100644 index 0000000..052a8d2 Binary files /dev/null and b/src/data/sound/book-dismiss.wav differ diff --git a/src/data/sound/click-1.wav b/src/data/sound/click-1.wav new file mode 100644 index 0000000..fdeff06 Binary files /dev/null and b/src/data/sound/click-1.wav differ diff --git a/src/data/sound/click-2.wav b/src/data/sound/click-2.wav new file mode 100644 index 0000000..528ebfc Binary files /dev/null and b/src/data/sound/click-2.wav differ diff --git a/src/data/sound/clicking.wav b/src/data/sound/clicking.wav new file mode 100644 index 0000000..3b99320 Binary files /dev/null and b/src/data/sound/clicking.wav differ diff --git a/src/data/sound/page-turn-1.wav b/src/data/sound/page-turn-1.wav new file mode 100644 index 0000000..8fd4096 Binary files /dev/null and b/src/data/sound/page-turn-1.wav differ diff --git a/src/data/sound/page-turn-2.wav b/src/data/sound/page-turn-2.wav new file mode 100644 index 0000000..cbcedd7 Binary files /dev/null and b/src/data/sound/page-turn-2.wav differ diff --git a/src/data/sound/page-turn-3.wav b/src/data/sound/page-turn-3.wav new file mode 100644 index 0000000..ab8e673 Binary files /dev/null and b/src/data/sound/page-turn-3.wav differ diff --git a/src/data/sound/regear.wav b/src/data/sound/regear.wav new file mode 100644 index 0000000..07d970c Binary files /dev/null and b/src/data/sound/regear.wav differ diff --git a/src/data/sound/slam.wav b/src/data/sound/slam.wav new file mode 100644 index 0000000..798d6e8 Binary files /dev/null and b/src/data/sound/slam.wav differ diff --git a/src/data/sound/switch.wav b/src/data/sound/switch.wav new file mode 100644 index 0000000..1d657dd Binary files /dev/null and b/src/data/sound/switch.wav differ diff --git a/src/data/sound/tick.wav b/src/data/sound/tick.wav new file mode 100644 index 0000000..4d1c442 Binary files /dev/null and b/src/data/sound/tick.wav differ diff --git a/src/data/sound/tock.wav b/src/data/sound/tock.wav new file mode 100644 index 0000000..8d4d616 Binary files /dev/null and b/src/data/sound/tock.wav differ diff --git a/src/data/sound/winding.wav b/src/data/sound/winding.wav new file mode 100644 index 0000000..8ecfa4a Binary files /dev/null and b/src/data/sound/winding.wav differ diff --git a/src/ext/FiraMono-Bold.woff b/src/ext/FiraMono-Bold.woff new file mode 100644 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 + + + + + + + + + + + + + + + + + + + + +
+
+

There was a problem. Sorry about that.

+

+

Error Log

+
+      
+
+
+

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

+

+

+ Supported browsers include recent versions of + Mozilla Firefox + and Google Chrome + on most desktop computers, + Chrome + for Android, and Safari on Mac OS X 10.7 and later + if + you enable WebGL manually. +

+

Error Log

+
+      
+
+
+
+

Pixel Witch Lesson #6

+ + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + +
+ + + + +
+

Press F12 to take a screenshot.

+
+
+
+

Pixel Witch Lesson #6

+
+
Designed & Implemented
+
Joe Wreschnig
+
Additional Programming
+
+ Brandon Jones & Colin MacKenzie IV + (glMatrix) +
+
+ Christoph Burgmer + (ayepromise) +
+
+ Ian McEwan, Ashima Arts + (WebGL Noise) +
+
+ Jorik Tangelder + (Hammer.js) +
+
Fonts
+
+ Carrois Type Design + (Fira) +
+
+ Dave Gandy + (Font Awesome) +
+
Special Thanks
+
Amelia Gorman
+
Jessicatz Fairymeadow
+
Kenney.nl
+
+ Richard + "The Clock Guy" + Oliver +
+
+
+
+

+ Copyright ©2014 + Yukkuri Games + and others +

+

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

+
+
+

+ View Complete Licensing Text +

+
+ + + + diff --git a/src/pwl6.css b/src/pwl6.css new file mode 100644 index 0000000..2b55cb0 --- /dev/null +++ b/src/pwl6.css @@ -0,0 +1,65 @@ +body { + background-color: black; +} + +.yuu-toast { + background-color: hsla(276, 33%, 10%, 1.0); + border: solid hsla(276, 33%, 48%, 1.0) 2px; + color: hsla(276, 33%, 95%, 1.0); +} + +.yuu-overlay { + background-color: hsla(276, 33%, 10%, 0.9); + border: solid hsla(276, 33%, 48%, 1.0) 2px; + color: hsla(276, 33%, 95%, 1.0); +} + +.yuu-overlay *:focus { + box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + -moz-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + -webkit-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + outline: none; +} + +input[type=range][data-yuu-command] { + background-color: hsla(276, 33%, 25%, 1.0); + border: solid hsla(276, 33%, 48%, 0.33) 1px; +} + +input[type=range][data-yuu-command]::-webkit-slider-thumb { + background-color: hsla(276, 33%, 65%, 1.0); +} + +input[type=range][data-yuu-command]::-moz-range-track { + background-color: hsla(276, 33%, 25%, 1.0); +} + +input[type=range][data-yuu-command]::-moz-range-thumb { + background: hsla(276, 33%, 65%, 1.0); +} + +input[type=checkbox][data-yuu-command]:focus + label[for] { + box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + -moz-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + -webkit-box-shadow: 0 0 5px hsla(276, 66%, 80%, 1.0); + outline: 0; +} + +input[data-yuu-command] + label[for]:before { + color: hsla(276, 33%, 65%, 1.0); +} + +a[href], [data-yuu-command] { + color: hsla(276, 33%, 65%, 1.0); +} + +hr { + border: solid hsla(276, 33%, 48%, 0.75) 1px; +} + +.link-footer { + list-style-type: none; + text-align: center; + margin-left: 0; + padding-left: 0; +} diff --git a/src/pwl6.js b/src/pwl6.js new file mode 100644 index 0000000..dc50206 --- /dev/null +++ b/src/pwl6.js @@ -0,0 +1,2149 @@ +"use strict"; + +var storage; +var SIGILS; +var BOOK; +var handScene; +var circleScene; +var NOISY_BLOCKS; + +var sounds; + +var TOP = 0; +var LEFT = 1; +var BOTTOM = 2; +var RIGHT = 3; + +var MenuScene, CircleScene, HandScene, BookScene, GridScene; + +yuu.Texture.DEFAULTS.magFilter = yuu.Texture.DEFAULTS.minFilter = "nearest"; + +var TICK_ROT = quat.rotateZ(quat.create(), quat.create(), Math.PI / 30); +var TICK_REV = quat.invert(quat.create(), TICK_ROT); +var TICK_ROT2 = quat.rotateZ(quat.create(), quat.create(), Math.PI / 60); + +function sawtooth (p) { + /** Sawtooth wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */ + var _2PI = 2 * Math.PI; + return 2 * (p / _2PI - Math.floor(0.5 + p / _2PI)); +} + +function triangle (p) { + /** Triangle wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */ + return 2 * Math.abs(sawtooth(p + Math.PI / 2)) - 1; +} + +function waveshift (period, peak, xoffset, yoffset) { + period /= 2 * Math.PI; + xoffset = xoffset || 0; + yoffset = yoffset || 0; + return function (f) { + return function (p) { + return yoffset + peak * f.call(this, (p + xoffset) / period); + }; + }; +} + +function cycler (scale) { + var f = waveshift(scale, 0.5, -Date.now(), 0.5)(triangle); + return function () { return f(Date.now()); }; +} + +function load () { + storage = ystorage.getStorage(); + yuu.audio.storage = storage; + + NOISY_BLOCKS = new yuu.ShaderProgram(null, ["@noise.glsl", "@noisyblocks"]); + + SIGILS = new yuu.Material("@sigils"); + BOOK = new yuu.Material("@book", NOISY_BLOCKS); + BOOK.uniforms.cut = yf.volatile(cycler(20000)); + BOOK.uniforms.range = 0.06; + BOOK.texture.ready.then(function (texture) { + BOOK.uniforms.resolution = new Float32Array( + [texture.width / 4, texture.height / 4]); + }); + + sounds = { + tick: new yuu.Instrument("@tick"), + tock: new yuu.Instrument("@tock"), + regear: new yuu.Instrument("@regear"), + winding: new yuu.Instrument("@winding"), + slam: new yuu.Instrument("@slam"), + switch: new yuu.Instrument("@switch"), + clicking: new yuu.Instrument("@clicking"), + bookAppear: new yuu.Instrument("@book-appear"), + switchBroke: new yuu.Instrument({ + sample: { "@switch": { duration: 0.27, offset: 0.1 } } }), + switchOn: new yuu.Instrument({ + sample: { "@switch": { duration: 0.2 } } }), + switchOff: new yuu.Instrument({ + sample: { "@switch": { offset: 0.2 } } }), + chime: new yuu.Instrument({ + envelope: { "0": 1, "0.7": 0.2, "3": 0 }, + modulator: { + envelope: { "0": 1, "0.7": 0.2, "3": 0 }, + frequency: "x1.5", + } + }), + }; + + yuu.director.pushScene(circleScene = new CircleScene()); + yuu.director.pushScene(handScene = new HandScene()); + yuu.director.pushScene(new MenuScene()); + if (!storage.getFlag("instructions")) { + yuu.director.entity0.attach(new yuu.Ticker(function () { + yuu.director.pushScene(new BookScene()); + }, 60)); + } + + return yuu.ready( + [SIGILS, BOOK] + .concat(yf.map(yf.getter.bind(sounds), Object.keys(sounds))) + ); +} + +function start () { + yuu.director.start(); +} + +window.addEventListener("load", function() { + yuu.registerInitHook(load); + yuu.init({ backgroundColor: [0, 0, 0, 1], antialias: false }).then(start); +}); + +var PALETTE = [[ 0.76, 0.13, 0.13 ], + [ 0.33, 0.49, 0.71 ], + [ 0.45, 0.68, 0.32 ], + [ 0.51, 0.32, 0.63 ], + [ 0.89, 0.49, 0.11 ], + [ 1.00, 1.00, 0.30 ]]; + +var LEVELS = [ + { name: "12345654321", + randomSlammer: [3, 5], + deps: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy" + }, + + { slammer: [1, 1], sets: "tutorial", + scramble: { easy: "01", hard: "0122" } }, + { slammer: [1, 1, 1], deps: "tutorial", + scramble: { easy: "11", hard: "1212" } }, + { slammer: [2, 1], deps: "tutorial", sets: "asymmetric", + scramble: { easy: "32", hard: "3321" } }, + { slammer: [1, 2, 1], deps: "tutorial", sets: "unequal", + scramble: { easy: "112", hard: "3210" } }, + { slammer: [2, 0], deps: "asymmetric", sets: "zero", + scramble: { easy: "23", hard: "032" } }, + { slammer: [2, 0, 2], deps: "zero", + scramble: { easy: "11", hard: "2211" } }, + { slammer: [1, 1, 1, 1], deps: "tutorial" }, + { slammer: [2, 1, 1], deps: "asymmetric" }, + { slammer: [1, 2, 1, 2], deps: "asymmetric", + scramble: { easy: "012" } }, + { slammer: [1, 2, 3, 4], deps: "asymmetric", sets: "solid", + scramble: { easy: "110" } }, + { slammer: [5, 0, 0, 4, 0], deps: "unequal zero", + scramble: { easy: "112" } }, + { slammer: [5, 3, 5, 3, 5], deps: "unequal solid", + scramble: { easy: "3232" } }, + { slammer: [4, 4, 4, 0, 4], deps: "solid zero", + scramble: { easy: "0321" } }, + { slammer: [1, 3, 0, 2], deps: "unequal zero" }, + { slammer: [1, 2, 3, 2, 1], deps: "unequal", + scramble: { easy: "3333" } }, + { slammer: [2, 0, 1, 2, 4], deps: "unequal zero" }, +]; + +function levelName (level) { + return (level.name || level.slammer.join("")).trim(); +} + +function wonLevel (level, difficulty) { + if (level.sets) + storage.setFlag(level.sets); + storage.setFlag(levelName(level) + "@" + difficulty); +} + +function hasBeaten (level, difficulty) { + return storage.getFlag(levelName(level) + "@" + difficulty); +} + +function scrambleForLevel (rnd, level, difficulty) { + var c = difficulty === "easy" ? 0 : 1; + if (difficulty === "random") + c = rnd.randrange(2, 5); + var length = level.slammer.length; + return rnd.randrange(length * c, length * (c + 1)) + 2; +} + +function difficultyForLevel (level) { + if (level.deps && !level.deps.split(" ").every(storage.getFlag, storage)) + return null; + if (hasBeaten(level, "hard")) + return "random"; + if (hasBeaten(level, "easy")) + return "hard"; + else + return "easy"; +} + +function levelRandom (level, difficulty) { + if (difficulty === "random") + return yuu.random; + else + return new yuu.Random(yuu.createLCG(+level.slammer.join(""))); +} + +function generateBoard (rnd, level) { + var size = level.length; + var board = new Array(size); + for (var i = 0; i < size; ++i) + board[i] = yf.repeat(i % PALETTE.length + 1, size); + if (rnd.randbool()) + yuu.transpose2d(board); + return board; +} + +function generateSlammer (rnd, level) { + var s = new Array(level.length); + for (var i = 0; i < s.length; ++i) + s[i] = yf.repeat(0, level[i]); + if (rnd.randbool()) + s.reverse(); + return s; +} + +var AnimationQueue = yT(yuu.C, { + constructor: function () { + this._queue = []; + }, + + attached: function () { + this._queue = []; + }, + + _runNext: function () { + var next = this._queue[0]; + if (next && this.entity) + this.entity.attach(new yuu.Animation( + next.timeline, next.params, this._complete.bind(this))); + }, + + _complete: function () { + var next = this._queue.shift(); + next.resolve(); + this._runNext(); + }, + + enqueue: function (timeline, params) { + return new Promise(function (resolve) { + this._queue.push({ + timeline: timeline, + params: params, + resolve: resolve + }); + // FIXME: Simply chaining the promise doesn't work here + // because the tick between the two handlers is often long + // enough to render a frame, and that frame will have some + // undesirable intermediate state. + if (this._queue.length === 1) + this._runNext(); + }.bind(this)); + }, + + SLOTS: ["animationQueue"] +}); + +var SLAMMER_ROTATE = { + 0: { tween1: { yaw: "yaw" }, duration: 10 } +}; + +var ROTATE_ALL = { + 0: { tweenAll: { yaw: "yaws" }, duration: 10 } +}; + +var SLAMMER_BOUNCE = { + 0: { tween1: { y: 0.5 }, duration: 5, repeat: -1 } +}; + +var SLIDE_BLOCKS = { + 0: { tweenAll: { position: "positions" }, + duration: 8, easing: "linear" }, +}; + +var SLAMMER_SLAM = { + 0: { tween1: { y: -1.5 }, easing: "linear", duration: 6 }, + 6: { event: "slideBoardBlocks" }, + 15: { event: "slam", + tween1: { y: 0 }, easing: "linear", duration: 8 } +}; + +var GRID_DISMISS = { + 0: { tween1: { yaw: 2 * Math.PI, x: "x", y: "y", scale: [0.3, 0.3, 1] }, + duration: 45 } +}; + +var GRID_FINISHED = { + 0: { tween: { arm: { scale: [0, 0, 1], yaw: "armYaw", y: "armY" }, + board: { y: "boardY" } }, + duration: 45 } +}; + +function rotateCw (d) { return (--d + 4) % 4; } +function rotateCcw (d) { return ++d % 4; } +function opposite (d) { return (d + 2) % 4; } + +var FlagSet = yT({ + /** Manage a set of semaphore-like counting flags. */ + + constructor: function () { + /** Construct a flag set for the provided flags. + + Flags are initialized to 0 by default. + */ + this._counts = {}; + for (var i = 0; i < arguments.length; ++i) + this._counts[arguments[i]] = 0; + }, + + increment: function () { + /** Increment the provided flags. */ + for (var i = 0; i < arguments.length; ++i) + this._counts[arguments[i]]++; + }, + + decrement: function () { + /** Decrement the provided flags. + + No underflow checks are performed. A flag with a negative + value is considered set exactly as a flag with a positive + value. + */ + for (var i = 0; i < arguments.length; ++i) + this._counts[arguments[i]]--; + }, + + some: function () { + /** Return true if any of the provided flags are set. */ + return yf.some.call(this._counts, yf.getter, arguments); + }, + + every: function () { + /** Return true if all of the provided flags are set. */ + return yf.every.call(this._counts, yf.getter, arguments); + }, + + none: function () { + /** Return true if none of the provided flags are set. */ + return !this.some.apply(this, arguments); + }, + + incrementer: function () { + /** Provide a bound 0-ary function to increment the provided flags. + + Useful for wrapps around context-free callbacks. + */ + var that = this, args = arguments; + return function () { that.increment.apply(that, args); }; + }, + + decrementer: function () { + /** Provide a bound 0-ary function to decrement the provided flags. + + Useful for wrapps around context-free callbacks. + */ + var that = this, args = arguments; + return function () { that.decrement.apply(that, args); }; + } +}); + +var BoardController = yT(yuu.C, { + constructor: function (rnd, level, colors) { + this.contents = generateBoard(rnd, level.slammer); + this.colors = colors; + }, + updateChildren: function () { + this.entity.data.quads.forEach(function (q) { + q.quad.position = [q.x, q.y]; + var i = this.contents[q.x][q.y]; + q.quad.color = this.colors[i]; + q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0]; + }, this); + }, + isComplete: function() { + var x, y; + var rows = true, cols = true; + for (x = 1; x < this.contents.length && rows; ++x) + for (y = 0; y < this.contents[x].length && rows; ++y) + rows = this.contents[x - 1][y] === this.contents[x][y]; + for (x = 0; x < this.contents.length && cols; ++x) + for (y = 1; y < this.contents[x].length && cols; ++y) + cols = this.contents[x][y - 1] === this.contents[x][y]; + return rows || cols; + }, + + shift: [ + function (x, replacement) { + var lost = this.contents[x].pop(); + this.contents[x].unshift(replacement); + return lost; + }, + function (y, replacement) { + yuu.transpose2d(this.contents); + var lost = this.shift[BOTTOM].call(this, y, replacement); + yuu.transpose2d(this.contents); + return lost; + }, + function (x, replacement) { + var lost = this.contents[x].shift(); + this.contents[x].push(replacement); + return lost; + }, + function (y, replacement) { + yuu.transpose2d(this.contents); + var lost = this.shift[TOP].call(this, y, replacement); + yuu.transpose2d(this.contents); + return lost; + } + ], + + SLOTS: ["controller"] +}); + +var SlammerController = yT(yuu.C, { + constructor: function (rnd, level, colors) { + this.blocks = generateSlammer(rnd, level.slammer); + this.orientation = TOP; + this.colors = colors; + this._undoRecord = []; + }, + isComplete: function() { + return yf.none(yf.some.bind(null, null), this.blocks); + }, + updateChildren: function () { + this.entity.data.quads.forEach(function (q) { + var i = this.blocks[q.x][q.y]; + q.quad.position = [q.x, q.y]; + q.quad.color = this.colors[i]; + q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0]; + }, this); + }, + + lastUndoRecord: { get: function () { + return yf.last(this._undoRecord); + } }, + + clearUndoRecord: function () { + this._undoRecord = []; + }, + + slam: function (board) { + var undoable = (this.orientation !== this.lastUndoRecord); + var length = this.blocks.length; + this.orientation = opposite(this.orientation); + this.blocks = yf.mapr.call(this, function (a, y) { + return yf.map(board.shift[this.orientation].bind(board, y), a) + .reverse(); + }, this.blocks, (this.orientation & 2) + ? yf.range(length) + : yf.range(length - 1, -1, -1)); + yf.each(function (i) { + i.x = length - (i.x + 1); + }, this.entity.data.quads); + this.updateChildren(); + board.updateChildren(); + if (undoable) + this._undoRecord.push(this.orientation); + else + this._undoRecord.pop(); + }, + + SLOTS: ["controller"] +}); + +function randSide (rnd, except) { + return (rnd || yuu.random).choice( + yf.without([TOP, LEFT, BOTTOM, RIGHT], except)); +} + +var HANDS_LEFT = { + 0: { tween: { left: { yaw: -0.3 } }, duration: 3 }, + 3: { tween: { left: { yaw: 0.0 } }, duration: 7 }, +}; + +var HANDS_RIGHT = { + 0: { tween: { right: { yaw: -0.3 } }, duration: 3 }, + 3: { tween: { right: { yaw: 0.0 } }, duration: 7 }, +}; + +var HANDS_UNDO = { + 0: { tween: { left: { yaw: 0.2 }, right: { yaw: 0.2 } }, + duration: 3 }, + 3: { tween: { left: { yaw: 0.0 }, right: { yaw: 0.0 } }, + duration: 7 } +}; + +var HANDS_MENU_CHOICE = { + 0: { tween: { left: { x: -1.3 }, + right: { x: -1.3 } }, + duration: 15, easing: "ease_in" + }, + + 10: { tween: { left: { scaleX: 1 }, + right: { scaleX: 1 } }, + duration: 20 }, + + 20: { set: { leftQuad: { color: "frontColor" }, + rightQuad: { color: "frontColor" } }, + tween: { left: { x: 0 }, right: { x: 0 } }, + duration: 15 + }, +}; + +var HANDS_RETURN = { + 0: { tween: { left: { x: -1.3 }, + right: { x: -1.3 } }, + duration: 20 + }, + + 10: { tween: { left: { scaleX: -1 }, + right: { scaleX: -1 } }, + duration: 20 }, + + 20: { set: { leftQuad: { color: "backColor" }, + rightQuad: { color: "backColor" } }, + tween: { left: { x: -1 }, right: { x: -1 } }, + duration: 10 + }, +}; + +var HANDS_SLAM = [ + // TOP + { 0: { tween: { left: { yaw: -0.2, scaleX: 0.8, y: -0.1 }, + right: { yaw: -0.2, scaleX: 0.8, y: -0.1 }, + }, duration: 10, repeat: -1 }, + }, + + // LEFT + { 0: { tween: { left: { scaleX: 0.8, x: 0.1 }, + right: { scaleX: 0.9 }, + }, duration: 10, repeat: -1 }, + }, + + // BOTTOM + { 0: { tween: { left: { yaw: 0.2, scaleX: 0.8 }, + right: { yaw: 0.2, scaleX: 0.8 }, + }, duration: 10, repeat: -1 }, + }, + + // RIGHT + { 0: { tween: { left: { scaleX: 0.9 }, + right: { scaleX: 0.8, x: 0.1 }, + }, duration: 10, repeat: -1 }, + }, +]; + +var HANDS_ROTATE_CW = { + 0: { tween: { left: { scaleX: 0.8 } }, duration: 5 }, + 5: { tween: { left: { scaleX: 1.0 } }, duration: 5 }, +}; + +var HANDS_ROTATE_CCW = { + 0: { tween: { right: { scaleX: 0.8 } }, duration: 5 }, + 5: { tween: { right: { scaleX: 1 } }, duration: 5 }, +}; + +var BUTTONS_IN = { + 0: { tween: { a: { x: 0 }, b: { x: 1.5 } }, duration: 25 } +}; + +var BUTTONS_OUT = { + 0: { tween: { a: { x: -1.5 }, b: { x: 0 } }, duration: 25 } +}; + +HandScene = yT(yuu.Scene, { + constructor: function () { + yuu.Scene.call(this); + var hands = new yuu.Material("@hand"); + this.left = new yuu.E(new yuu.Transform()); + var l = new yuu.E( + new yuu.Transform([-0.5, 0.5, 0]), + new yuu.DataC({ command: "left" }), + this.leftQuad = new yuu.QuadC(hands)); + this.left.addChild(l); + var r = new yuu.E( + new yuu.Transform([-0.5, 0.5, 0]), + new yuu.DataC({ command: "right" }), + this.rightQuad = new yuu.QuadC(hands)); + this.right = new yuu.E(new yuu.Transform()); + this.right.addChild(r); + var SIZE_X = yuu.random.gauss(1.2, 0.15) * 0.35; + var SIZE_Y = yuu.random.gauss(1.1, 0.05) * 0.51; + var hand = yuu.random.randrange(3); + this.leftQuad.texBounds = this.rightQuad.texBounds = [ + hand / 2.99, 0, (hand + 1) / 3.01, 1]; + this.layer0.resize(-0.75, 0, 1.5, 1.5); + var leftWrist = new yuu.E( + new yuu.Transform([-0.20, 0, 0], null, + [SIZE_X, SIZE_Y, 1])); + var rightWrist = new yuu.E( + new yuu.Transform([0.20, 0, 0], null, + [-SIZE_X, SIZE_Y, 1])); + leftWrist.addChild(this.left); + rightWrist.addChild(this.right); + this.addEntities(leftWrist, rightWrist); + this.backColor = yuu.hslToRgb( + (yuu.random.gauss(0.1, 0.1) + 10) % 1, + yuu.random.uniform(0.2, 0.7), + yuu.random.uniform(0.2, 0.6), + 1.0); + this.leftQuad.alpha = this.rightQuad.alpha = 0.2; + var hsl = yuu.rgbToHsl(this.backColor); + hsl[2] = hsl[2].lerp(1, 0.15); + hsl[1] = hsl[1].lerp(0, 0.30); + hsl[3] = 0.4; + this.frontColor = yuu.hslToRgb(hsl); + this.leftQuad.color = this.rightQuad.color = this.frontColor; + this.ready = hands.ready; + + function Button (i, command) { + return new yuu.E( + new yuu.Transform(), + new yuu.DataC({ command: command }), + new yuu.QuadC(SIGILS) + .setTexBounds([i / 6, 0, (i + 1) / 6, 0.5]) + .setColor(PALETTE[i])); + } + + this.helpButton = new Button(1, "help"); + this.backButton = new Button(3, "back"); + this.backButton.transform.x -= 1.5; + this.leftButtons = new yuu.E(new yuu.Transform()); + this.leftButtons.addChildren(this.helpButton, this.backButton); + this.rightButton = new Button(2, "showOverlay preferences"); + this.leftButtons.transform.scale + = this.rightButton.transform.scale + = [0.075, 0.075, 1]; + this.entity0.addChildren(this.leftButtons, this.rightButton); + this.buttons = [this.helpButton, this.backButton, this.rightButton, + l, r]; + }, + + inputs: { + resize: function () { + var base = new yuu.AABB(-0.75, 0, 0.75, 1.5); + var vp = base.matchAspectRatio(yuu.viewport); + vp.y1 -= vp.y0; + vp.y0 = 0; + this.leftButtons.transform.xy = [ + vp.x0 + this.leftButtons.transform.scaleX, + vp.y1 - this.leftButtons.transform.scaleY]; + this.rightButton.transform.xy = [ + vp.x1 - this.rightButton.transform.scaleX, + vp.y1 - this.rightButton.transform.scaleY]; + this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h); + }, + + mousemove: function (p) { + p = this.layer0.worldFromDevice(p); + this.cursor = ""; + for (var i = 0; i < this.buttons.length; ++i) { + if (this.buttons[i].transform.contains(p)) { + this.cursor = "pointer"; + } + } + }, + + tap: function (p) { + p = this.layer0.worldFromDevice(p); + for (var i = 0; i < this.buttons.length; ++i) { + if (this.buttons[i].transform.contains(p)) { + yuu.director.execute(this.buttons[i].data.command); + return true; + } + } + }, + + doubletap: function () { + return this.inputs.tap.apply(this, arguments); + }, + }, + + _anim: function (timeline) { + this.entity0.attach(new yuu.Animation( + timeline, { + left: this.left.transform, + right: this.right.transform, + leftQuad: this.leftQuad, + rightQuad: this.rightQuad, + frontColor: this.frontColor, + backColor: this.backColor + })); + }, + + undo: function () { this._anim(HANDS_UNDO); }, + movedLeft: function () { this._anim(HANDS_LEFT); }, + movedRight: function () { this._anim(HANDS_RIGHT); }, + slam: function (o) { this._anim(HANDS_SLAM[o]); }, + rotatedCw: function () { this._anim(HANDS_ROTATE_CW); }, + rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW); }, + menuChoice: function () { + this.entity0.attach(new yuu.Animation( + BUTTONS_IN, { + a: this.backButton.transform, + b: this.helpButton.transform + })); + this._anim(HANDS_MENU_CHOICE); + }, + finished: function () { + this.entity0.attach(new yuu.Animation( + BUTTONS_OUT, { + a: this.backButton.transform, + b: this.helpButton.transform + })); + this._anim(HANDS_RETURN); + }, +}); + +var GRID_APPEAR = { + 0: { set1: { y: 5 }, + tween1: { y: 0 }, duration: 10 }, +}; + +GridScene = yT(yuu.Scene, { + constructor: function (level, difficulty) { + yuu.Scene.call(this); + this.entity0.attach(new yuu.Transform()); + this.level = level; + this.difficulty = difficulty; + this._locks = new FlagSet("slam", "spin", "quit"); + var rnd = levelRandom(level, difficulty); + var colors = yuu.random.shuffle(PALETTE.slice()); + colors.unshift([1.0, 1.0, 1.0]); + this.board = new yuu.E(new BoardController(rnd, level, colors), + new yuu.Transform(), + new yuu.DataC({ quads: [] })); + this.slammer = new yuu.E(new SlammerController(rnd, level, colors), + new yuu.Transform(), + new yuu.DataC({ quads: [] })); + this.slammerHead = new yuu.E(new yuu.Transform()); + this.slammerRoot = new yuu.E(new yuu.Transform()); + var length = level.slammer.length; + var maxSize = length * length; + var slammerBatch = new yuu.QuadBatchC(maxSize); + slammerBatch.material = SIGILS; + var boardBatch = new yuu.QuadBatchC(maxSize); + boardBatch.material = SIGILS; + this.slammerRoot.transform.xy = [length / 2 - 0.5, length / 2 - 0.5]; + this.slammerHead.transform.xy = [-length / 2 + 0.5, length / 2 + 2]; + this.slammerRoot.addChild(this.slammerHead); + this.slammerHead.addChild(this.slammer); + this.slammer.attach(slammerBatch); + this.board.attach(boardBatch); + yf.irange.call(this, function (x) { + yf.irange.call(this, function (y) { + var quad = boardBatch.createQuad(); + quad.color = colors[this.board.controller.contents[x][y]]; + quad.position = [x, y]; + this.board.data.quads.push({ quad: quad, x: x, y: y }); + }, length); + }, length); + + for (var x = 0; x < this.slammer.controller.blocks.length; ++x) { + for (var y = 0; y < this.slammer.controller.blocks[x].length; ++y) { + var quad = slammerBatch.createQuad(); + quad.color = colors[this.slammer.controller.blocks[x][y]]; + quad.position = [x, y]; + this.slammer.data.quads.push({ quad: quad, x: x, y: y }); + } + } + this.addEntities(this.board, this.slammerRoot); + this.scramble(rnd); + if (!(this.cheating = yuu.director.input.pressed["`"])) { + this.slammer.controller.clearUndoRecord(); + } + this._dragging = 0; + + this.gridBB = new yuu.AABB(-0.5, -0.5, length - 0.5, length - 0.5); + this.leftBB = new yuu.AABB( + -Infinity, this.gridBB.y0, this.gridBB.x0, this.gridBB.y1); + this.rightBB = new yuu.AABB( + this.gridBB.x1, this.gridBB.y0, Infinity, this.gridBB.y1); + this.topBB = new yuu.AABB( + this.gridBB.x0, this.gridBB.y1, this.gridBB.x1, Infinity); + this.bottomBB = new yuu.AABB( + this.gridBB.x0, -Infinity, this.gridBB.x1, this.gridBB.y0); + }, + + init: function () { + this._locks.increment("slam"); + this.entity0.attach(new yuu.Animation( + GRID_APPEAR, { $: this.slammer.transform }, + this._locks.decrementer("slam"))); + }, + + scramble: function (rnd) { + var scramble = (this.level.scramble || {})[this.difficulty]; + var slammerCon = this.slammer.controller; + var boardCon = this.board.controller; + if (!scramble) { + var count = scrambleForLevel(rnd, this.level, this.difficulty); + while (this.isComplete()) { + var c = count; + while (c--) { + slammerCon.orientation = randSide(rnd, slammerCon.orientation); + slammerCon.slam(boardCon); + } + } + } else { + for (var i =0; i < scramble.length; ++i) { + slammerCon.orientation = +scramble[i]; + slammerCon.slam(boardCon); + } + } + slammerCon.orientation = randSide(); + this.slammerRoot.transform.yaw = slammerCon.orientation * Math.PI / 2; + }, + + isComplete: function () { + return this.slammer.controller.isComplete() + && this.board.controller.isComplete(); + }, + + rotateTo: yuu.cmd(function (orientation) { + return new Promise(function (resolve) { + if (this._locks.some("spin")) + return this; + this.slammer.controller.orientation = orientation; + this._locks.increment("slam"); + var yaw0 = this.slammerRoot.transform.yaw; + var yaw1 = orientation * Math.PI / 2; + + sounds.clicking.play(); + this.entity0.attach(new yuu.Animation( + SLAMMER_ROTATE, { + $: this.slammerRoot.transform, + yaw: yaw0 + yuu.normalizeRadians(yaw1 - yaw0) + }, function () { + this._locks.decrement("slam"); + resolve(this); + }.bind(this))); + }.bind(this)); + }, "", "move the slammer to the top"), + + rotateCw: yuu.cmd(function () { + handScene.rotatedCw(); + circleScene.rotated(); + this.rotateTo(rotateCw(this.slammer.controller.orientation)); + }, "", "rotate the active piece clockwise"), + + rotateCcw: yuu.cmd(function () { + handScene.rotatedCcw(); + circleScene.rotated(); + this.rotateTo(rotateCcw(this.slammer.controller.orientation)); + }, "", "rotate the active piece counter-clockwise"), + + left: yuu.cmd(function () { this.rotateCw(); }), + right: yuu.cmd(function () { this.rotateCcw(); }), + + undo: yuu.cmd(function (v) { + var con = this.slammer.controller; + var _ = function () { this.undo(this._undo); }.bind(this); + if ((this._undo = v) && con.lastUndoRecord !== undefined) { + if (con.orientation !== con.lastUndoRecord) { + circleScene.reverse(); + handScene.undo(); + this.rotateTo(con.lastUndoRecord) + .then(this.slam.bind(this)) + .then(_); + } else { + this.slam().then(_); + } + } + }, "", "rotate the active piece counter-clockwise"), + + checkWon: function () { + if (this.isComplete() && !this._locks.some("quit")) { + this._locks.increment("quit", "slam", "spin"); + var firstTime = !hasBeaten(this.level, this.difficulty); + if (!this.cheating) + wonLevel(this.level, this.difficulty); + var scene = new MenuScene(this.level); + yuu.director.pushScene(scene); + scene.didWinLevel(this.level, this.difficulty, firstTime); + this.entity0.attach(new yuu.Animation( + GRID_FINISHED, { + arm: this.slammerRoot.transform, + armYaw: this.slammerRoot.transform.yaw + 3 * Math.PI, + armY: this.slammerRoot.transform.y + 1.5, + board: this.board.transform, + boardY: this.level.slammer.length * 3 + }, function () { + yuu.director.removeScene(this); + }.bind(this) + )); + } + }, + + slideBoardBlocks: function (anim, params) { + var dx = 0, dy = 0; + var orientation = this.slammer.controller.orientation; + switch (orientation) { + case LEFT: dx = 1.5; break; + case TOP: dy = -1.5; break; + case RIGHT: dx = -1.5; break; + case BOTTOM: dy = 1.5; break; + } + var sgnx = Math.sign(dx); + var sgny = Math.sign(dy); + var $s = []; + var positions = []; + var blocks = this.slammer.controller.blocks; + this.slammer.data.quads.forEach(function (q) { + var d = blocks[q.x].length; + $s.push(q.quad); + positions.push([q.quad.position[0], q.quad.position[1] - d]); + }, this); + this.board.data.quads.forEach(function (q) { + var x = orientation === TOP ? q.x : blocks.length - (q.x + 1); + var y = orientation === LEFT ? q.y : blocks.length - (q.y + 1); + + $s.push(q.quad); + positions.push([q.quad.position[0] + sgnx * blocks[y].length, + q.quad.position[1] + sgny * blocks[x].length]); + }, this); + this.entity0.attach(new yuu.Animation(SLIDE_BLOCKS, { + $s: $s, + positions: positions + })); + }, + + slam: yuu.cmd(function () { + var r = new Promise(function (resolve, reject) { + if (this._locks.some("slam")) { + reject("slamming is locked"); + return; + } + this._locks.increment("spin", "slam"); + circleScene.slam(); + sounds.slam.play(); + handScene.slam(this.slammer.controller.orientation); + this.entity0.attach(new yuu.Animation( + SLAMMER_SLAM, { + $: this.slammer.transform, + slam: function () { + this._locks.decrement("spin"); + this.slammer.controller.slam(this.board.controller); + this.slammerRoot.transform.yaw = Math.PI / 2 * + this.slammer.controller.orientation; + }.bind(this), + slideBoardBlocks: this.slideBoardBlocks.bind(this) + }, function () { + this.checkWon(); + this._locks.decrement("slam"); + resolve(this); + }.bind(this))); + }.bind(this)); + return r; + }, "", "slam the active piece"), + + back: yuu.cmd(function (x, y) { + if (this._locks.some("quit")) + return; + this._locks.increment("quit", "slam", "spin"); + var scene = new MenuScene(this.level); + yuu.director.pushScene(scene); + var v = [x || yuu.random.uniform(-1, 1), + y || yuu.random.uniform(-1, 1)]; + var size = this.board.controller.contents.length * 5; + vec2.scale(v, vec2.normalize(v, v), size); + this.entity0.attach(new yuu.Animation( + GRID_DISMISS, { + $: this.entity0.transform, + x: v[0], y: v[1] + }, function () { + yuu.director.removeScene(this); + }.bind(this) + )); + circleScene.lose(); + }, "", "go back to the menu"), + + slammerBB: { get: function (p) { + var length = this.level.slammer.length; + switch (this.slammer.controller.orientation) { + case LEFT: + return new yuu.AABB(-Infinity, -0.5, -1, length - 0.5); + case RIGHT: + return new yuu.AABB(length + 1, -0.5, Infinity, length - 0.5); + case TOP: + return new yuu.AABB(-0.5, length + 1, length - 0.5, Infinity); + case BOTTOM: + return new yuu.AABB(-0.5, -Infinity, length - 0.5, -1); + } + } }, + + _swipe: function (p0, p1) { + p0 = this.layer0.worldFromDevice(p0); + p1 = this.layer0.worldFromDevice(p1); + if (this.slammerBB.contains(p0)) { + this.slam(); + return true; + } + if (this.gridBB.contains(p0) && !this.gridBB.contains(p1)) { + this.back(p1.x - p0.x, p1.y - p0.y); + return true; + } + }, + + inputs: { + resize: function () { + var length = this.level.slammer.length; + var base = new yuu.AABB(-length - 2.5, -length - 2.5, + 2 * length + 1.5, 2 * length + 1.5); + var vp = base.matchAspectRatio(yuu.viewport); + this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h); + }, + + tap: function (p) { + p = this.layer0.worldFromDevice(p); + if (this.gridBB.contains(p)) { + this.slam(); + return true; + } + }, + + touch: function (p) { + var length = this.level.slammer.length; + var middle = (length - 1) / 2; + p = this.layer0.worldFromDevice(p); + if (this.slammerBB.contains(p)) { + this.slammer.attach(new yuu.Animation( + SLAMMER_BOUNCE, { $: this.slammer.transform })); + } else if (this.leftBB.contains(p)) { + this.rotateTo(LEFT); + handScene.rotatedCw(); + return true; + } else if (this.rightBB.contains(p)) { + this.rotateTo(RIGHT); + handScene.rotatedCcw(); + return true; + } else if (this.topBB.contains(p)) { + this.rotateTo(TOP); + if (p.x < middle) + handScene.rotatedCw(); + else + handScene.rotatedCcw(); + return true; + } else if (this.bottomBB.contains(p)) { + this.rotateTo(BOTTOM); + if (p.x < middle) + handScene.rotatedCw(); + else + handScene.rotatedCcw(); + return true; + } + }, + + doubletap: function () { + return this.inputs.tap.apply(this, arguments); + }, + + hold: function (p) { + p = this.layer0.worldFromDevice(p); + if (this.gridBB.contains(p)) { + this.undo(true); + return true; + } + }, + + dragstart: function (p) { + p = this.layer0.worldFromDevice(p); + this._dragging = this.slammerBB.contains(p); + }, + + drag: function (p0, p1) { + var p = this.layer0.worldFromDevice(p1); + if (this._dragging && !this._locks.some("slam")) { + var inGrid = this.gridBB.contains(p); + var length = this.level.slammer.length; + var o; + if (this._dragging === true && inGrid) { + this.slam(); + } else if (p.x > 0 && p.x < length && !inGrid) { + o = p.y < 0 ? BOTTOM : TOP; + if (o !== this.slammer.controller.orientation) { + this.rotateTo(o); + this._dragging = 2; + } + } else if (p.y > 0 && p.y < length && !inGrid) { + o = p.x < 0 ? LEFT : RIGHT; + if (o !== this.slammer.controller.orientation) { + this.rotateTo(o); + this._dragging = 2; + } + } + } + return this._dragging; + }, + + dragend: function (p0, p1) { + this._dragging = false; + }, + + release: function () { + this.undo(false); + }, + + swipeleft: function (p0, p1) { + return this._swipe(p0, p1); + }, + swiperight: function (p0, p1) { + return this._swipe(p0, p1); + }, + swipeup: function (p0, p1) { + return this._swipe(p0, p1); + }, + swipedown: function (p0, p1) { + return this._swipe(p0, p1); + }, + }, + + KEYBINDS: { + w: "rotateCw", + a: "rotateCcw", + s: "rotateCcw", + d: "rotateCw", + left: "rotateCcw", + right: "rotateCw", + up: "rotateCw", + down: "rotateCcw", + shift: "slam", + space: "slam", + z: "slam", + backspace: "+undo", + c: "+undo", + escape: "back", + back: "back", + gamepadbutton0: "slam", + gamepadbutton1: "+undo", + gamepadbutton2: "slam", + gamepadbutton3: "+undo", + gamepadbutton4: "rotateCcw", + gamepadbutton5: "rotateCw", + gamepadbutton8: "back", + gamepadbutton14: "rotateCcw", + gamepadbutton15: "rotateCw", + } +}); + +var MENU_APPEAR = { + 0: [{ set1: { x: 5, y: 5, scaleX: 0, scaleY: 0 } }, + { tween1: { x: 0, y: 0, scaleX: 1 }, duration: 24 }, + { tween1: { scaleY: 1 }, + duration: 55, easing: yuu.Tween.METASPRING(1, 10)}], +}; + +var MENU_SLIDE = { + 0: { tween1: { x: "x" }, duration: "duration" } +}; + +var FLASH = { + 0: { tween1: { luminance: 1, alpha: 1 }, duration: 32, repeat: -1 } +}; + +var MENU_SLAM = { + 0: { tween: { cursor: { y: "mid" } }, duration: 8 }, + 8: { tween: { cursor: { y: "line" }, + select: { y: -1.0 } + }, duration: 12 }, + 20: { tween: { scene: { y: 10 }, + select: { y: -11.5, scale: [3, 3, 1] } + }, duration: 18 }, + 38: { event: "appear", + tween: { select: { y: 0, scale: [1, 0, 1] } }, + duration: 20 } +}; + +function menuEntityForLevel (level, i) { + var activated = false; + function randomizeSlammer () { + var min = level.randomSlammer[0]; + var max = level.randomSlammer[1]; + var size = yuu.random.randrange(min, max + 1); + level.slammer = []; + do { + for (var i = 0; i < size; ++i) + level.slammer[i] = yuu.random.randrange(0, size); + } while (Math.min.apply(Math, level.slammer) === max + || Math.max.apply(Math, level.slammer) === 0); + } + + function generateQuads() { + batch.disposeAll(); + var rgb = PALETTE[i % PALETTE.length]; + var fit = level.slammer.length + 1; + batch.createQuad().color = rgb; + level.slammer.forEach(function (size, y) { + var c = batch.createQuad(); + c.color = [0, 0, 0]; + c.alpha = hasBeaten(level, "easy") ? 0.5 : 1.0; + c.size = [size / fit, 1 / fit]; + c.position = [0, -0.5 + (y + 1) / fit]; + }); + ce.data.flasher = batch.createQuad(); + ce.data.flasher.alpha = 0; + return ce; + } + + if (level.randomSlammer) + randomizeSlammer(); + + // 14 = maximum slammer size + 1 background + 1 flasher + var batch = new yuu.QuadBatchC(14); + var ce = new yuu.E( + new yuu.Transform([2 * i, 0, 0]), + batch, + new yuu.DataC({ + activate: function () { + activated = true; + var scene = new GridScene(level, difficultyForLevel(level)); + yuu.director.insertUnderScene(scene); + } + }) + ); + + if (level.randomSlammer) { + ce.attach(new yuu.Ticker(function () { + if (!activated && yuu.random.randbool(0.7)) { + randomizeSlammer(); + ++i; + generateQuads(); + } + return !activated; + }, 30, 15)); + } + + generateQuads(); + return ce; +} + +var HAND_TICK_BACK = { + 0: { tween1: { rotation: "rotation" }, duration: 6, repeat: -1 } +}; + +MenuScene = yT(yuu.Scene, { + constructor: function (initialLevel) { + yuu.Scene.call(this); + this.entity0.attach(new yuu.Transform(), + new AnimationQueue()); + + this.pointer = new yuu.E( + new yuu.Transform([5, 8, 0]), + new yuu.QuadC()); + + var menu = this.menu = new yuu.E(new yuu.Transform([5, 6.5, 0])); + this.addEntities(menu, this.pointer); + this.availableLevels = LEVELS.filter(difficultyForLevel); + this.availableLevels + .map(menuEntityForLevel) + .forEach(menu.addChild, menu); + + var initialIdx = this.availableLevels.indexOf(initialLevel); + this._locks = new FlagSet("slam", "move"); + this.activeIndex = Math.max(initialIdx, 0); + menu.transform.x = 5 - 2 * this.activeIndex; + this.changeActiveIndex(this.activeIndex, false); + this._dragStartX = null; + + this.entity0.attach( + new yuu.Ticker(this._animation.bind(this), 60)); + }, + + _animation: function (count) { + var length = this.availableLevels.length; + var range = Math.pow(2, length); + var rand = yuu.random.randrange(range); + var targets = []; + var yaws = []; + for (var i = 0; i < length; ++i) { + var child = this.menu.children[i]; + var level = this.availableLevels[i]; + var won = hasBeaten(level, "hard"); + if ((won || ((count ^ i) & 1)) && ((count ^ rand) & (1 << i))) { + var dyaw = won + ? yuu.random.randsign(Math.PI / 2) + : -Math.PI / 2; + targets.push(child.transform); + yaws.push(child.transform.yaw + dyaw); + } + } + if (targets.length) { + this.entity0.attach(new yuu.Animation( + ROTATE_ALL, { $s: targets, yaws: yaws })); + } + circleScene.clockTick(TICK_ROT2, HAND_TICK_BACK); + sounds[["tick", "tock"][count & 1]] + .createSound(yuu.audio, yuu.audio.currentTime, 0, 0.2, 1.0) + .connect(yuu.audio.music); + + return true; + }, + + init: function () { + circleScene.toBottom(); + handScene.finished(); + this._locks.increment("slam", "move"); + this.entity0.animationQueue.enqueue( + MENU_APPEAR, + { $: this.entity0.transform }) + .then(this._locks.decrementer("slam", "move")); + }, + + didWinLevel: function (level, difficulty, firstTime) { + var idx = this.availableLevels.indexOf(level); + circleScene.win(); + if (firstTime) + this.entity0.animationQueue.enqueue( + FLASH, { $: this.menu.children[idx].data.flasher }); + for (var i = idx; i < this.availableLevels.length; ++i) { + if (!hasBeaten(this.availableLevels[i], difficulty)) { + this._locks.increment("move"); + this.changeActiveIndex(i, true) + .then(this._locks.decrementer("move")); + + break; + } + } + }, + + changeActiveIndex: function (index, animate) { + var oldIndex = this.activeIndex; + var p; + this.activeIndex = index = yf.clamp( + index, 0, this.menu.children.length - 1); + if (index !== oldIndex && animate) { + this._locks.increment("slam"); + var duration = Math.ceil(8 * Math.abs(oldIndex - index)); + p = this.entity0.animationQueue.enqueue( + MENU_SLIDE, { + $: this.menu.transform, + x: 5 - 2 * index, + duration: duration + }); + p.then(this._locks.decrementer("slam")); + } + return p || Promise.resolve(); + }, + + left: yuu.cmd(function () { + if (!this._locks.some("move")) { + sounds[this.activeIndex === 0 ? "switchBroke" : "switch"].play(); + handScene.movedLeft(); + this.changeActiveIndex(this.activeIndex - 1, true); + } + }, "move the cursor left"), + right: yuu.cmd(function () { + if (!this._locks.some("move")) { + sounds[this.activeIndex === this.availableLevels.length - 1 + ? "switchBroke" : "switch"].play(); + handScene.movedRight(); + this.changeActiveIndex(this.activeIndex + 1, true); + } + }, "move the cursor right"), + + slam: yuu.cmd(function () { + if (this._locks.some("slam")) + return; + var activeChild = this.menu.children[this.activeIndex]; + this._locks.increment("slam", "move"); + handScene.menuChoice(); + circleScene.toBack(); + circleScene.slam(); + sounds.winding.play(); + this.entity0.animationQueue.enqueue( + MENU_SLAM, { + cursor: this.pointer.transform, + select: activeChild.transform, + scene: this.entity0.transform, + mid: this.pointer.transform.y - 0.5, + line: this.pointer.transform.y - 1.5, + appear: activeChild.data.activate + }).then(function () { + this._locks.decrementer("slam", "move"); + yuu.director.removeScene(this); + }.bind(this)); + }, "choose the active menu item"), + + inputs: { + resize: function () { + var base = new yuu.AABB(0, 0, 10, 10); + var vp = base.matchAspectRatio(yuu.viewport); + this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h); + }, + + pinchout: function (p0, p1) { + p0 = this.layer0.worldFromDevice(p0); + p1 = this.layer0.worldFromDevice(p1); + if (vec2.sqrDist(p0, p1) > 1) { + this.slam(); + return true; + } + }, + + hold: function (p) { + return this.inputs.dragstart.call(this, p); + }, + + release: function (p) { + if (this._dragStartX !== null) + return this.inputs.dragend.call(this, p); + }, + + dragstart: function (p) { + if (this._locks.some("move")) + return false; + p = this.layer0.worldFromDevice(p); + if (p.y > 6 && p.y < 8.5 && p.inside && this._dragStartX === null) { + sounds.switchOn.play(); + this._locks.increment("move"); + this._dragStartX = this.menu.transform.x; + return true; + } + }, + + dragdown: function (p0, p1) { + p0 = this.layer0.worldFromDevice(p0); + p1 = this.layer0.worldFromDevice(p1); + + if (p0.x >= 4.5 && p0.x <= 5.5 + && p0.y >= 6.0 && p0.y <= 8.5 + && p0.y - p1.y > 1) { + this.slam(); + return true; + } + }, + + drag: function (p0, p1) { + if (this._dragStartX !== null) { + p0 = this.layer0.worldFromDevice(p0); + p1 = this.layer0.worldFromDevice(p1); + this.menu.transform.x = this._dragStartX + (p1.x - p0.x); + var index = Math.round((5 - this.menu.transform.x) / 2); + this.changeActiveIndex(index); + return true; + } + }, + + dragend: function (p0, p1) { + if (this._dragStartX !== null) { + sounds.switchOff.play(); + this._locks.decrement("move"); + this._dragStartX = null; + var index = this.activeIndex; + this.activeIndex = (5 - this.menu.transform.x) / 2; + this.changeActiveIndex(index, true); + return true; + } + }, + + tap: function (p) { + p = this.layer0.worldFromDevice(p); + if (p.y > 6 && p.y < 7 && p.inside) { + var dx = Math.round((p.x - 5) / 2); + if (dx === 0) this.slam(); + else if (dx < 0) handScene.movedLeft(); + else if (dx > 0) handScene.movedRight(); + var idx = this.activeIndex; + this.changeActiveIndex(this.activeIndex + dx, true); + if (idx !== this.activeIndex) + sounds.switch.play(); + else + sounds.switchBroke.play(); + return true; + } + + }, + + doubletap: function (p) { + p = this.layer0.worldFromDevice(p); + if (p.x >= 4.5 && p.x <= 5.5 && p.y >= 6.0 && p.y <= 8.5) { + this.slam(); + return true; + } + }, + }, + + resetEverything: yuu.cmd(function () { + storage.clear(); + yuu.director.stop(); + start(); + }, "reset all saved data"), + + unlock: yuu.cmd(function (d) { + LEVELS.forEach(function (level) { wonLevel(level, d); }); + yuu.director.pushPopScene(new MenuScene()); + }, "", "unlock all levels to the given difficulty"), + + KEYBINDS: { + left: "left", + right: "right", + up: "right", + down: "left", + w: "right", + a: "left", + s: "left", + d: "right", + shift: "slam", + space: "slam", + z: "slam", + "`+r+e": "resetEverything", + "`+u+e": "unlock easy", + "`+u+h": "unlock hard", + gamepadbutton0: "slam", + gamepadbutton8: "help", + gamepadbutton9: "slam", + gamepadbutton13: "slam", + gamepadbutton14: "left", + gamepadbutton15: "right", + } +}); + + +var BOOK_APPEAR = { + 0: { set1: { y: 1.5, x: -1.5 }, + tween: { bgQuad: { alpha: 0.75 }, $: { y: 0, x: 0 }, }, + duration: 30 } +}; + +var BOOK_DISMISS = { + 0: { tween: { bgQuad: { alpha: 0 }, $: { y: 1.5, x: -1.5, } }, + duration: 30 } +}; + +var KEYBOARD_PAGE = [0.25, 0.50, 0.50, 1.00]; +var POINTERS_PAGE = [0.25, 0.00, 0.50, 0.50]; +var GAMEPAD_PAGE = [0.00, 0.00, 0.25, 0.50]; + +var BOOK_FORWARD = [ + { 0: { set: { page2Quad: { color: [0.2, 0.2, 0.2, 1], texBounds: "page" } }, + tween: { page1: { x: -1/3 / 2, scaleX: 0 }, + page2: { x: +1/3 / 2 }, + page2Quad: { color: [1, 1, 1, 1] }, + }, duration: 15, easing: "linear" }, + 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] }, + page2Quad: { z: 0, texBounds: "page" } }, + tween: { page1: { x: -1/3, scaleX: -2/3 }, + page2: { x: +1/3 } + }, duration: 15, easing: "linear" }, + }, + + { 0: { tween: { page1: { x: -1/3 / 2 }, + page2: { x: +1/3 / 2, scaleX: 0 } + }, duration: 15, easing: "linear" }, + 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] }, + page2Quad: { z: 1, texBounds: [1.00, 0.5, 0.75, 1] } }, + tween: { page1Quad: { color: [0.2, 0.2, 0.2, 1] }, + page1: { x: 0 }, + page2: { x: 0, scaleX: -2/3 }, + }, duration: 15, easing: "linear" }, + }, + + BOOK_DISMISS +]; + +var BOOK_BACKWARD = [ + { 0: { tween: { page1: { x: -1/3 / 2, scaleX: 0 }, + page2: { x: +1/3 / 2 }, + }, duration: 15, easing: "linear" }, + 15: { set: { page1Quad: { z: 1, texBounds: [0.50, 0.5, 0.75, 1] }, + page2Quad: { z: 0 } }, + tween: { page2Quad: { color: [0.2, 0.2, 0.2, 1] }, + page1: { x: 0, scaleX: 2/3 }, + page2: { x: 0 }, + }, duration: 15, easing: "linear" }, + }, + + { 0: { set: { page1Quad: { color: [0.2, 0.2, 0.2, 1] } }, + tween: { page1Quad: { color: [1.0, 1.0, 1.0, 1] }, + page1: { x: -1/3 / 2 }, + page2: { x: +1/3 / 2, scaleX: 0 } + }, duration: 15, easing: "linear" }, + + 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] }, + page2Quad: { z: 0, texBounds: "page" } }, + tween: { page1: { x: -1/3 }, + page2: { x: +1/3, scaleX: 2/3 }, + }, duration: 15, easing: "linear" }, + }, +]; + +BookScene = new yT(yuu.Scene, { + constructor: function () { + yuu.Scene.call(this); + var bg = new yuu.E( + new yuu.Transform().setScale([20, 20, 1]), + this.bgQuad = new yuu.QuadC() + .setColor([0, 0, 0, 0]) + .setZ(-1)); + this.page1 = new yuu.E(new yuu.Transform(), + this.page1Quad = new yuu.QuadC(BOOK)); + this.page1Quad.texBounds = [0.50, 0.5, 0.75, 1]; + this.page1Quad.z = 1; + this.page2 = new yuu.E(new yuu.Transform(), + this.page2Quad = new yuu.QuadC(BOOK)); + this.page2Quad.texBounds = [0.25, 0.5, 0.50, 1]; + this.page1.transform.scale = [2/3, 1, 1]; + this.page2.transform.scale = [2/3, 1, 1]; + this.entity0.attach(new yuu.Transform()); + this.current = 0; + this._locks = new FlagSet("turn"); + this.addEntities(bg, this.page1, this.page2); + + this.dismissSound = new yuu.Instrument("@book-dismiss"); + this.pageSounds = [new yuu.Instrument("@page-turn-1"), + new yuu.Instrument("@page-turn-2"), + new yuu.Instrument("@page-turn-3")]; + + this.ready = yuu.ready([this.dismissSound].concat(this.pageSounds)); + }, + + help: yuu.cmd(function () { + this.skip(); + }, "dismiss the help screen"), + + licensing: yuu.cmd(function () { + var licensing = document.getElementById("yuu-licensing"); + var parent = licensing.parentNode; + var spinner = document.createElement("div"); + spinner.className = "yuu-spinner"; + spinner.id = licensing.id; + parent.replaceChild(spinner, licensing); + Promise.all( + yf.map(yuu.GET, + [yuu.PATH + "data/license.txt", "data/license.txt"])) + .then(function (texts) { + var text = texts.join("\n-- \n\n"); + var p = document.createElement("pre"); + p.textContent = text; + p.id = spinner.id; + parent.replaceChild(p, spinner); + }); + }, "why would you ever want to run this?"), + + init: function () { + this._anim(BOOK_APPEAR); + storage.setFlag("instructions"); + }, + + _anim: function (anim) { + this._locks.increment("turn"); + // FIXME: Need hooks from animations to audio + var completion = this._locks.decrementer("turn"); + switch (anim) { + case BOOK_DISMISS: + this.dismissSound.play(); + completion = yuu.director.removeScene.bind(yuu.director, this); + break; + case BOOK_APPEAR: + sounds.bookAppear.play(); + break; + default: + yuu.random.choice(this.pageSounds).play(); + break; + } + + var device = yuu.director.preferredDevice(); + this.entity0.attach(new yuu.Animation( + anim, { + $: this.entity0.transform, + page: device === "keyboard" ? KEYBOARD_PAGE + : device === "gamepad" ? GAMEPAD_PAGE + : POINTERS_PAGE, + page1: this.page1.transform, + page2: this.page2.transform, + page1Quad: this.page1Quad, + page2Quad: this.page2Quad, + bgQuad: this.bgQuad + }, completion)); + }, + + advance: yuu.cmd(function () { + if (this._locks.some("turn")) + return; + this._anim(BOOK_FORWARD[this.current++]); + }), + + skip: yuu.cmd(function () { + if (this._locks.some("turn")) + return; + this._anim(BOOK_DISMISS); + }), + + back: yuu.cmd(function () { + if (this._locks.some("turn")) + return; + if (this.current > 0) + this._anim(BOOK_BACKWARD[--this.current]); + }), + + LOGOTYPE: new yuu.AABB(-0.16, -0.41, 0.12, -0.33), + COLOPHON: new yuu.AABB(-0.06, -0.41, 0.11, -0.28), + + inputs: { + resize: function () { + var base = new yuu.AABB(-0.7, -0.55, 0.7, 0.55); + var vp = base.matchAspectRatio(yuu.viewport); + this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h); + }, + + mousemove: function (p) { + p = this.layer0.worldFromDevice(p); + if (this.current === BOOK_FORWARD.length - 1 + && this.LOGOTYPE.contains(p)) { + this.cursor = "pointer"; + } else if (this.current === 0 && this.COLOPHON.contains(p)) { + this.cursor = "pointer"; + } else if (this.current === 0 || p.x >= -0.2) { + this.cursor = ""; + } else { + this.cursor = "W-resize"; + } + }, + + tap: function (p) { + p = this.layer0.worldFromDevice(p); + if (this.current === BOOK_FORWARD.length - 1 + && this.LOGOTYPE.contains(p)) { + yuu.openURL("http://www.yukkurigames.com/"); + } else if (this.current === 0 && this.COLOPHON.contains(p)) { + yuu.director.showOverlay("colophon"); + } else if (this.current === 0 || p.x >= -0.2) { + this.advance(); + } else { + this.back(); + } + return true; + }, + swipeleft: function (event) { this.advance(); return true; }, + swiperight: function (event) { this.back(); return true; }, + dragleft: function (event) { this.advance(); return true; }, + dragright: function (event) { this.back(); return true; }, + swipeup: function (event) { this.skip(); return true; }, + dragup: function (event) { this.skip(); return true; }, + + consume: yuu.Director.prototype.GESTURES + .concat(yuu.Director.prototype.CANVAS_EVENTS) + }, + + KEYBINDS: { + space: "advance", + shift: "advance", + z: "advance", + x: "advance", + right: "advance", + left: "back", + back: "skip", + escape: "skip", + gamepadbutton0: "advance", + gamepadbutton1: "skip", + gamepadbutton4: "back", + gamepadbutton5: "advance", + gamepadbutton8: "skip", + gamepadbutton9: "skip", + gamepadbutton14: "back", + gamepadbutton15: "advance", + } +}); + +var OUTER_FLIP_TICK = { + 0: { tween1: { yaw: "yaw" }, duration: 15 } +}; + +var CIRCLE_TO_BOTTOM = { + 0: { tween1: { pitch: Math.PI * 0.35, y: -0.3 }, duration: 35 } +}; + +var CIRCLE_TO_BACK = { + 0: { tween1: { pitch: Math.PI * 0.15, y: -0.1 }, duration: 35 } +}; + +var CIRCLE_INNER_RATCHET = { + 0: { tween1: { rotation: "rotation1" }, duration: 15 }, + 10: { tween1: { rotation: "rotation2" }, duration: 10 }, + 20: { tween1: { rotation: "rotation1" }, duration: 20, + easing: yuu.Tween.STEPPED(5) }, + 40: { tween1: { rotation: "rotation2" }, duration: 15 } +}; + +var CIRCLE_INNER_WIND = { + 0: { tween1: { rotation: "rotation1" }, duration: 8 }, + 15: { tween1: { rotation: "rotation2" }, duration: 20 }, +}; + +var BACKGROUND_DRIFT = { + 0: [{ tween1: { yaw: Math.PI * 2 }, + duration: 13 * 60 * 60, repeat: -Infinity, easing: "linear" }, + { tween1: { scaleX: 0.5 }, + duration: 11 * 60 * 60, repeat: -Infinity }, + { tween1: { scaleY: 0.5 }, + duration: 7 * 60 * 60, repeat: -Infinity }] +}; + +var HAND_TICK = { + 0: { tween1: { rotation: "rotation" }, duration: 6 } +}; + +var CHIMES = [ + // Nearly all derived from + // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html + // + // All transposition & transcription errors are mine. + + { name: "Westminster", + keys: ["D4", "E4"], + bars: ["0 2 1 -3", + "0 1 2 0", + "2 0 1 -3", + "2 1 0 -3", + "-3 1 2 0"] + }, + + { name: "Wittington", + keys: ["Eb4", "E4"], + bars: ["1 2 3 5 4 6 7 0", + "1 3 5 7 6 4 2 0", + "3 1 2 4 5 6 7 0", + "4 3 5 2 6 1 7 0", + "6 7 2 5 4 1 3 0", + "7 1 6 2 5 3 4 0", + "7 3 2 1 4 5 6 0", + "7 3 6 2 5 1 4 0", + "7 5 3 1 6 4 2 0", + "7 5 6 4 1 3 2 0", + "7 6 3 2 5 4 1 0", + "7 6 5 4 3 2 1 0"] + }, + + { name: "Canterbury", + keys: ["D4", "E4"], + bars: ["2 0 5 3 1 4", + "3 5 1 4 0 2", + "3 5 4 2 1 0", + "5 3 1 4 2 0", + "1 3 5 2 0 4", + "0 5 3 1 2 4", + "5 3 1 2 4 0"] + }, + + { name: "Trinity", + keys: ["F3", "D4"], + bars: ["5 4 3 2 1 0", + "2 4 3 1 2 0", + "5 3 4 2 1 0", + "4 3 2 1 5 2", + "5 0 4 3 2 1"] + }, + + /* + { name: "St. Michael's", + keys: ["F3", "C4"], + bars: ["7 6 5 4 3 2 1 0", + "7 1 2 3 6 4 5 0", + "4 3 2 5 1 6 7 0", + "6 7 2 3 1 4 5 0", + "4 6 2 7 3 1 5 0"] + }, + */ + + { name: "Winchester", + keys: ["C4", "E4"], + bars: ["5 3 1 0 2 4", + "0 1 3 5 4 2", + "5 3 1 4 2 0", + "1 2 5 4 0 1", + "5 1 3 2 4 0"] + } + +]; + +function third (s) { + return "Q " + s.replace(/([^ ]+ [^ ]+)/g, "$1 Z"); +} + +function silence (s) { + return "Q " + s.replace(/[^ ]+/g, "Z"); +} + +var TIMES1 = ["Q", "H", "H", "H", "H.", "H.", "W", "W"]; +var TIMES2 = ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", + third, third, "Q.", "Q.", + "H", "H"]; +var TIMES3 = ["Q", "Q", silence, silence, + third, third, third, third, + "Q.", "Q.", + "H", "H"]; + +function deck (pack, random) { + random = random || yuu.random; + var stock = []; + return function () { + if (stock.length === 0) + stock = random.shuffle(pack.slice()); + return stock.pop(); + }; +} + +function generateScore () { + var chimes = yuu.random.choice(CHIMES); + var bar = deck(chimes.bars); + function draw (t) { + return yf.isFunction(t) ? t(bar()) : t + " " + bar(); + } + + function line (times) { + return yf.map(draw, yuu.random.shuffle(times)).join(" "); + } + + var track = "{ - W HZ " + line(TIMES1) + + " { W HZ Z " + line(TIMES2) + + " { W HZ Z Z I Z " + line(TIMES3); + var key = yuu.random.choice(chimes.keys); + yuu.log("messages", "Playing " + chimes.name + " in " + key + " major."); + var score = yuu.parseScore(track, yuu.Scales.MAJOR, key); + score.key = key; + return score; +} + +CircleScene = yT(yuu.Scene, { + constructor: function () { + yuu.Scene.call(this); + this.layer0.resize(-0.6, -0.6, 1.2, 1.2); + var arm = this.arm = new yuu.E(new yuu.Transform()); + this.outer = new yuu.E( + new yuu.Transform([Math.sqrt(2) / 5, -Math.sqrt(2) / 5, 0]), + this.outerQuad = new yuu.QuadC(new yuu.Material("@circle-outer")) + .setZ(1) + .setLuminance(0.4) + .setSize([0.35417, 0.35417])); + arm.addChild(this.outer); + + var rim = new yuu.E( + new yuu.Transform(), + this.rimQuad = new yuu.QuadC(new yuu.Material("@circle-rim")) + .setLuminance(0.2)); + var inner = this.inner = new yuu.E( + new yuu.Transform(), + this.innerQuad = new yuu.QuadC(new yuu.Material("@circle-inner")) + .setLuminance(0.3)); + + var NOISY_QUADS = new yuu.ShaderProgram( + ["@noise.glsl", "@noisyquads"], ["yuu/@color"]); + + var bgMat = new yuu.Material( + yuu.Texture.DEFAULT, NOISY_QUADS, { range: 0.8 }); + bgMat.uniforms.cut = yf.volatile(cycler(100000)); + var DIM = 16; + var batch = new yuu.QuadBatchC(DIM * DIM); + batch.material = bgMat; + var bg = new yuu.E(new yuu.Transform(), batch); + yf.irange(function (x) { + yf.irange(function (y) { + var quad = batch.createQuad(); + quad.size = [1/4, 1/4]; + quad.position = [(x - DIM / 2) * 1/4, + (y - DIM / 2) * 1/4]; + quad.color = [0.12, 0.08, 0.16]; + quad.texBounds = yf.repeat(x * DIM + y, 4); + }, DIM); + }, DIM); + + this.entity0.addChild(bg); + this.entity0.attach(new yuu.Animation( + BACKGROUND_DRIFT, { $: bg.transform })); + + this.ground = new yuu.E(new yuu.Transform()); + this.ground.addChildren(rim, inner, arm); + this.entity0.addChild(this.ground); + + this.music = yuu.audio.createGain(); + this.music.gain.value = 0.3; + this.music.connect(yuu.audio.music); + this._finished = false; + + this.ready = yuu.ready([ + this.outerQuad.material, + this.innerQuad.material, + this.rimQuad.material, + bgMat.ready + ]); + }, + + help: yuu.cmd(function () { + yuu.director.pushScene(new BookScene()); + }, "bring up the help screen"), + + yuu: yuu.cmd(function () { + this.outerQuad.material = new yuu.Material("@circle-outer-ee"); + }, "yuu~"), + + KEYBINDS: { + slash: "help", + f1: "help", + gamepadbutton6: "help", + f10: "showOverlay preferences", + "shift+y+u+`": "yuu", + "gamepadbutton10+gamepadbutton11": "yuu", + }, + + inputs: { + resize: function () { + var vp = new yuu.AABB(-0.6, -0.6, 0.6, 0.6) + .matchAspectRatio(yuu.viewport); + this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h); + } + }, + + toBottom: function () { + this.entity0.attach( + new yuu.Animation(CIRCLE_TO_BOTTOM, { $: this.ground.transform })); + }, + + wind: function () { + var rot1 = this.inner.transform.rotation; + quat.rotateZ(rot1, rot1, Math.PI / (2 * Math.E)); + var rot2 = quat.rotateX(quat.create(), rot1, -Math.PI / 2); + quat.rotateY(rot2, rot2, Math.PI / 2); + quat.rotateX(rot2, rot2, -Math.PI / 2); + this.entity0.attach( + new yuu.Animation(CIRCLE_INNER_WIND, { + $: this.inner.transform, + rotation1: rot1, + rotation2: rot2 + })); + this.tension = 0.5; + this.reversed = 0; + var score = []; + score.key = this.score && this.score.key; + this.score = score; + }, + + _musicSchedule: function (count) { + var t = yuu.director.currentAudioTime; + var note; + + if (this._finished) { + if (this._finished === "won" && this.score.key) { + var score = yuu.parseScore( + yuu.random.choice([ + "1 3 2 Z 0 { - 1 Z 2 Z 0", + "1 2 3 Z 0 { - 1 Z 3 Z 0", + "0 1 2 Z 4 { - 0 Z 2 Z 4", + ]), + yuu.Scales.MAJOR, this.score.key); + while ((note = score.shift())) { + sounds.chime.createSound( + yuu.audio, + t + note.time / 4, + note.hz, + 1.0, note.duration + ).connect(this.music); + } + } + this._finished = false; + return false; + } + + if (!(this.score && this.score.length)) { + this.score = generateScore(); + this.playing = 0; + } + + ++this.playing; + while (this.score.length && this.score[0].time < this.playing) { + note = this.score.shift(); + sounds.chime.createSound( + yuu.audio, + t + note.time % 1 + yuu.random.gauss(0, 0.015), + note.hz, + 1.0, note.duration + ).connect(this.music); + } + + if ((this.tension *= 0.95) > 1) { + this.tension /= 2; + sounds.winding.createSound(yuu.audio, t, 0, 1.0, 1.0) + .connect(this.music); + var flip = !this.outer.transform.yaw * yuu.random.randsign(Math.PI); + this.entity0.attach( + new yuu.Animation(OUTER_FLIP_TICK, { + $: this.outer.transform, + yaw: flip + })); + } else { + [sounds.tick, sounds.tock][count & 1] + .createSound(yuu.audio, t, 0, 0.5, 1.0) + .connect(this.music); + } + + this.clockTick(this.reversed-- > 0 ? TICK_REV : TICK_ROT); + + return true; + }, + + clockTick: function (amount, anim) { + var rot = this.arm.transform.rotation; + quat.multiply(rot, rot, amount || TICK_ROT); + this.arm.attach(new yuu.Animation( + anim || HAND_TICK, + { $: this.arm.transform, rotation: rot })); + }, + + toBack: function () { + this.wind(); + this.entity0.attach( + new yuu.Animation(CIRCLE_TO_BACK, { $: this.ground.transform })); + + this.playing = 4; + this.arm.attach( + new yuu.Ticker(this._musicSchedule.bind(this), 60)); + }, + + win: function () { + this._finished = "won"; + this.wind(); + this.entity0.attach( + new yuu.Animation(FLASH, { $: this.innerQuad }), + new yuu.Animation(FLASH, { $: this.rimQuad }, null, 32), + new yuu.Animation(FLASH, { $: this.outerQuad }, null, 48) + ); + }, + + lose: function () { + this._finished = "lose"; + var rot1 = this.inner.transform.rotation; + quat.rotateZ(rot1, rot1, -Math.PI / Math.E); + var rot2 = quat.rotateZ(quat.create(), rot1, Math.PI / Math.E); + this.entity0.attach( + new yuu.Animation(CIRCLE_INNER_RATCHET, { + $: this.inner.transform, + rotation1: rot1, + rotation2: rot2 + })); + sounds.regear.play(); + }, + + rotated: function () { + this.tension += yuu.random.uniform(0.1); + }, + + slam: function () { + this.tension += yuu.random.uniform(0.2); + }, + + reverse: function () { + this.tension -= yuu.random.uniform(0.1); + this.reversed = Math.max(this.reversed, 0) + 1; + } + +}); diff --git a/src/yuu/audio.js b/src/yuu/audio.js new file mode 100644 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..8b16707 --- /dev/null +++ b/src/yuu/core.js @@ -0,0 +1,896 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + + yuu.require = function (m) { + try { return require(m); } + catch (exc) { return null; } + }; + + if (!Math.sign) + require("./pre"); + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + var gui = yuu.require("nw.gui"); + var fs = yuu.require("fs"); + var stringLerp = this.stringLerp || yuu.require("string-lerp"); + + var initHooks = []; + var initOptions = null; + + if (typeof document !== "undefined") { + var scripts = document.getElementsByTagName('script'); + var path = yf.last(scripts).src.split('?')[0]; + yuu.PATH = path.split('/').slice(0, -1).join('/') + '/'; + } else { + yuu.PATH = "file://" + escape(module.__dirname) + "/"; + } + + yuu.registerInitHook = initHooks.push.bind(initHooks); + /** Register a hook to be called during Yuu initialization + + Hooks are called in registration order with the module and + the options dictionary passed to the init method. (This is + also set to the module.) + */ + + function showError (exc, kind) { + var prefix = "yuu-" + (kind || "") + "error"; + yuu.logError(exc); + var dialog = document.getElementById(prefix); + var errorMessage = document.getElementById(prefix + "-message"); + if (errorMessage) + errorMessage.textContent = exc.message; + var errorStack = document.getElementById(prefix + "-stack"); + if (errorStack) + errorStack.textContent = exc.message + "\n\n" + exc.stack; + return dialog; + } + yuu.showError = showError; + + function fatalError (exc) { + var dialog = showError(exc, "fatal-"); + if (dialog) + dialog.style.display = "block"; + if (gui) { + gui.Window.get().show(); + gui.Window.get().focus(); + } + throw exc; + } + + yuu.init = function (options) { + /** Initialize Yuu and call all registered hooks + */ + + if (gui) { + var win = gui.Window.get(); + var nativeMenuBar = new gui.Menu({ type: "menubar" }); + if (nativeMenuBar.createMacBuiltin) { + nativeMenuBar.createMacBuiltin( + document.title, { hideEdit: true }); + win.menu = nativeMenuBar; + } + var wkdoc = document; + win.on("minimize", function () { + var ev = new Event("visibilitychange"); + wkdoc.hidden = true; + wkdoc.dispatchEvent(ev); + }); + win.on("restore", function () { + var ev = new Event("visibilitychange"); + wkdoc.hidden = false; + wkdoc.dispatchEvent(ev); + }); + } + + return new Promise(function (resolve) { + // TODO: Some kind of loading progress bar. + initOptions = options || {}; + yuu.log("messages", "Initializing Yuu engine."); + var promises = []; + yf.each(function (hook) { + promises.push(hook.call(yuu, initOptions)); + }, initHooks); + initHooks = null; // Bust future registerInitHook calls. + yuu.log("messages", "Initialization hooks complete."); + if (gui) { + gui.Window.get().show(); + gui.Window.get().focus(); + } + resolve(Promise.all(yf.filter(null, promises))); + }).then(function () { + yuu.log("messages", "Loading complete."); + }).catch(fatalError); + }; + + yuu.log = yf.argv(function (category, args) { + /** Log a message to the console. + + This supports simple filtering by setting e.g. + `yuu.log.errors = true` to log anything with the + `"errors"` category. + */ + if (!category || this.log[category]) { + switch (category) { + case "errors": return console.error.apply(console, args); + case "warnings": return console.warn.apply(console, args); + default: return console.log.apply(console, args); + } + } + }); + + yuu.log.errors = true; + yuu.log.warnings = true; + yuu.log.messages = true; + + yuu.logError = function (e) { + yuu.log("errors", e.message || "unknown error", e); + }; + + yuu.GET = function (url, params) { + /** Promise the HTTP GET the contents of a URL. */ + return new Promise(function (resolve, reject) { + var req = new XMLHttpRequest(); + req.open("GET", url, true); + for (var k in params) + req[k] = params[k]; + req.onload = function () { + var status = this.status; + // status === 0 is given by node-webkit for success. + if ((status >= 200 && status < 300) || status === 0) + resolve(this.response); + else + reject(new Error( + url + ": " + status + ": " + this.statusText)); + }; + req.onabort = function () { reject(new Error("aborted")); }; + req.onerror = function () { reject(new Error("network error")); }; + req.ontimeout = function () { reject(new Error("timed out")); }; + req.send(null); + }); + }; + + yuu.Image = function (src) { + /** Promises a DOM Image. */ + return new Promise(function (resolve, reject) { + var img = new Image(); + img.onload = function () { + resolve(img); + }; + img.onerror = function () { + var msg = "Unable to load " + img.src; + yuu.log("errors", msg); + reject(new Error(msg)); + }; + img.src = src; + }); + }; + + /** Command parsing and execution + + The command API serves several roles. It is a way to enable or + disable different game logic within different scenes; capture + and replay or automate game events; loosely or late-bind game + modules; customize input mappings; and a debugging tool to + help inspect or modify the state of a running program. + + A command is a string of a command name followed by arguments + separated by whitespace. It's similar to a fully bound + closure. It is less flexible but easier to inspect, store, + replay, and pass around. + + Command names are mapped to functions, grouped into sets, and + the sets pushed onto a stack. They are executed by passing the + command string to the execute function which walks the stack + looking for the matching command. + + If the command string is prefaced with + or -, true or false + are appended to the argument list. e.g. `+command` is + equivalent to `command true` and `-command 1 b` is equivalent + to `command 1 b false`. By convention, commands of those forms + return their internal state when called with neither true or + false. This is useful for another special prefix, ++. When + called as `++command`, it is executed with no arguments, the + result inverted (with !), and then called again passing that + inverted value as the last argument. + */ + + function isCommand (f) { + return yf.isFunction(f) && f._isCommandFunction; + } + + function cmdbind () { + // Commands are practically a subtype of functions. Binding + // them (which happens often, e.g. when Scenes register + // commands) should also return a command. + var f = Function.prototype.bind.apply(this, arguments); + // usage is still valid iff no new arguments were given. + return cmd(f, arguments.length <= 1 && this.usage, this.description); + } + + var cmd = yuu.cmd = yf.argcd( + /** Decorate a function for command execution + + Command functions need some special attributes to work + correctly. This decorator makes sure they have them. + */ + function (f) { return yuu.cmd(f, null, null); }, + function (f, description) { return yuu.cmd(f, null, description); }, + function (f, usage, description) { + f._isCommandFunction = true; + f.usage = usage || " ".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..b572735 --- /dev/null +++ b/src/yuu/data/yuu.css @@ -0,0 +1,364 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +@font-face { + font-family: 'FontAwesome'; + src: url('../../ext/font-awesome.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-UltraLight.woff'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-UltraLightItalic.woff'); + font-weight: 200; + font-style: italic; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Regular.woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Italic.woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-Bold.woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Fira Sans'; + src: url('../../ext/FiraSans-BoldItalic.woff'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: 'Fira Mono'; + src: url('../../ext/FiraMono-Regular.woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Fira Mono'; + src: url('../../ext/FiraMono-Bold.woff'); + font-weight: 700; + font-style: normal; +} + +pre, tt, code, kbd { + font-family: 'Fira Mono', FontAwesome, monospace; +} + +body { + overflow: hidden; + margin: 0; + padding: 0; + font-family: 'Fira Sans', FontAwesome, sans-serif; +} + +#yuu-canvas { + /* Specifying only width/height gives incorrect results on Chrome + 33.0.1750.152 when fullscreen on > 1 devicePixelRatio. The + canvas takes on the correct size, but is centered in a page of + e.g. 2x for 2 DPR so you only see the top-left quadrant. + + Specifying only top/bottom/left/right 0 also breaks, because in the + absence of a CSS size the browser tries to set the client size to + the canvas buffer size, which means it grows/shrinks by the DPR + every resize event. + + Specifying all six attributes makes it work as desired. */ + position: absolute; + width: 100%; + height: 100%; + bottom: 0; + left: 0; + right: 0; + top: 0; +} + +#yuu-canvas:focus { + outline: inherit; +} + +#yuu-licensing { + padding-left: 2em; + padding-right: 2em; + text-align: center; + font-size: 0.7em; +} + +pre#yuu-licensing { + text-align: left; +} + +[data-yuu-command] { + cursor: pointer; +} + +/* Animations */ + +.yuu-from-top-right { + transform: translate(50vw, -110%) !important; + -webkit-transform: translate(50vw, -110%) !important; +} + +.yuu-from-top { + transform: translate(-50%, -110%) !important; + -webkit-transform: translate(-50%, -110%) !important; +} + +.yuu-fade { + opacity: 0 !important; +} + +.yuu-squish { + margin-left: 0 !important; + margin-right: 0 !important; + max-width: 0 !important; + min-width: 0 !important; + overflow: hidden !important; + padding-left: 0em !important; + padding-right: 0em !important; +} + +/* Toasts are short-lived feedback to user actions. */ +.yuu-toast { + pointer-events: none; + background-color: rgba(50, 50, 50, 0.5); + border-radius: 0.2em; + border: solid rgba(255, 255, 255, 0.5) 1px; + color: #eee; + display: table-cell; + float: right; + font-size: 3em; + margin-left: 0.125em; + margin-right: 0.125em; + margin-top: 0.25em; + max-width: 60%; + /* Minimum size is a square: 1.25 + 0.125 * 2 for padding = 1.5em, + same as the line height. */ + min-width: 1.4em; + padding: 0 0.125em; + padding-top: 0.15em; + position: relative; + text-align: center; + transition: all 0.5s; + -webkit-transition: all 0.5s; +} + +/* Overlays are hidden HTML-based scenes that the director can load. + These appear over the game, and are modal. The primary use case is + configuration menus, copyright information, error feedback, + etc. */ + +.yuu-overlay { + background-color: rgba(50, 50, 50, 0.9); + border-radius: 0.2em; + border: solid rgba(255, 255, 255, 0.9) 1px; + color: #eee; + display: none; + width: 60%; + max-width: 600px; + min-width: 300px; + margin-left: auto; + margin-right: auto; + left: 50%; + max-height: 80%; + overflow: auto; + padding: 0 1em 1em 1em; + position: fixed; + transform: translate(-50%, 10vh) scale(1, 1); + -webkit-transform: translate(-50%, 10vh) scale(1, 1); + transition: transform 0.3s, opacity 0.3s; + -webkit-transition: -webkit-transform 0.3s, opacity 0.3s; +} + +/* Overlays are focusable but should not show it - they are always + somewhere in the event tree when visible. */ +.yuu-overlay:focus { + outline: inherit; +} + +.yuu-overlay h1 { + font-size: 1.2em; + font-weight: normal; + text-align: center; +} + +.yuu-overlay h2 { + font-size: 1.1em; + font-weight: normal; +} + +.yuu-overlay hr { + margin-bottom: 1em; + margin-top: 0.5em; +} + +/* For consistency overlays use custom CSS for controls, which + means we need a default focused behavior. */ +.yuu-overlay *:focus { + outline: solid grey 1px; +} + +div[data-yuu-command=dismiss] { + font-size: 1.5em; + width: 1.25em; + height: 1.25em; + text-align: center; + position: fixed; + margin-left: -0.6667em; +} + +div[data-yuu-command=dismiss]:after { + content: "\f00d"; +} + +/* Table layout for options screens. In general, two or three columns, + the leftmost is a simple control, the middle/last is a label, and + the last is a more complicated control like a range or select + dropdown. */ + +.yuu-options { + border-collapse: separate; + border-spacing: 0.25em; +} + +.yuu-options td:first-child { + min-width: 2em; + white-space: nowrap; +} + +.yuu-options td:last-child { + width: 100%; +} + +/* De/re-style checkboxes. This means hiding the actual + checkbox and making it tiny, and instead filling in the + label immediately after it. */ +input[type=checkbox][data-yuu-command] { + max-width: 0; + opacity: 0; +} + +input[type=checkbox][data-yuu-command] + label[for] { + cursor: pointer; + display: inline-block; + text-align: center; + width: 1.3333em; + font-size: 1.25em; +} + +input[type=checkbox][data-yuu-command] + label[for]:before { + display: inline-block; + padding-top: 0.2em; +} + +input[type=checkbox][data-yuu-command] + label[for]:before { + content: "\f096"; +} + +input[type=checkbox][data-yuu-command]:checked + label[for]:before { + content: "\f046"; +} + +input[type=checkbox][data-yuu-command]:focus + label[for] { + outline: solid grey 1px; +} + +/* De/re-style ranges. */ +input[type=range][data-yuu-command] { + -webkit-appearance: none; + background-color: gray; +} + +input[type=range][data-yuu-command]::-moz-range-track { + background: gray; + border: none; + outline: none; +} + +input[type=range][data-yuu-command]::-webkit-slider-thumb { + -webkit-appearance: none; + background-color: #444; + width: 1.5em; + height: 1em; +} + +input[type=range][data-yuu-command]::-moz-range-thumb { + border: none; + background-color: #444; + width: 1.5em; + height: 1em; +} + +/* Special-case icons for the mute checkbox. */ + +input[type=checkbox][data-yuu-command=mute]:checked + label[for]:before { + content: "\f026"; +} + +input[type=checkbox][data-yuu-command=mute] + label[for]:before { + content: "\f028"; +} + +@-moz-keyframes spin { + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(360deg); } +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.yuu-spinner:after { + content: "◔"; + -webkit-animation: spin 1s linear infinite; + -moz-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; + display: inline-block; +} + +dl { + text-align: center; +} + +dt { + margin-top: 1em; + margin-bottom: 0.25em; + font-size: 0.8em; + font-weight: 200; +} + +dd { + margin-left: 0; +} diff --git a/src/yuu/director.js b/src/yuu/director.js new file mode 100644 index 0000000..8723014 --- /dev/null +++ b/src/yuu/director.js @@ -0,0 +1,696 @@ +/* Copyright 2014 Yukkuri Games + Licensed under the terms of the GNU GPL v2 or later + @license http://www.gnu.org/licenses/gpl-2.0.html + @source: http://yukkurigames.com/yuu/ +*/ + +(function (yuu) { + "use strict"; + + var yT = this.yT || require("./yT"); + var yf = this.yf || require("./yf"); + + // It's vaguely plausible to want a director without any scenes + // (only entity0 and the canvas), which means the renderer is not + // required. + if (!yuu.E) require("./ce"); + if (!yuu.InputState) require("./input"); + if (!yuu.Material) require("./gfx"); + + yuu.Director = yT({ + constructor: function (commandStack, input, tickHz) { + /** Manage and update a set of Scenes + + The director is responsible for calling two functions + regularly on the Scene instances it controls, `tick` and + `render`. `tick` is called at a regular interval (or at + least pretends to be called at one, browser scheduler + permitting), and `render` is called when the browser asks + for a new display frame. + */ + this._scenes = []; + this.entity0 = new yuu.E(); + this._commandStack = commandStack || yuu.commandStack; + this.input = input || new yuu.InputState([yuu.defaultKeybinds]); + this._events = {}; + this._tickCount = 0; + this._timerStart = 0; + this._audioOffset = 0; + this._rafId = null; + this._tickHz = tickHz || 60; + this._afterRender = []; + + this.commands = yuu.extractCommands(this); + this._commandStack.push(this.commands); + this._dogesture = this.__dogesture.bind(this); + this._gesture = null; + this._resized = false; + this._toasts = {}; + this._devices = {}; + }, + + pushScene: function (scene) { + /** Add a Scene onto the director's stack */ + this.insertScene(scene, this._scenes.length); + }, + + popScene: function () { + /** Remove the top scene from the director's stack */ + this.removeScene(yf.last(this._scenes)); + }, + + pushPopScene: function (scene) { + /** Replace the top scene on the stack */ + this.popScene(); + this.pushScene(scene); + }, + + insertScene: function (scene, idx) { + var scenes = this._scenes.slice(); + scenes.splice(idx, 0, scene); + this._scenes = scenes; + this._commandStack.insertBefore( + scene.commands, + this._scenes[idx + 1] && this._scenes[idx + 1].commands); + this.input.insertBefore( + scene.keybinds, + this._scenes[idx + 1] && this._scenes[idx + 1].keybinds); + scene.init(this); + if (scene.inputs.resize) + scene.inputs.resize.call(scene, yuu.canvas); + }, + + insertUnderScene: function (scene, over) { + return this.insertScene(scene, this._scenes.indexOf(over)); + }, + + removeScene: function (scene) { + /** Remove a Scene onto the director's stack */ + this._scenes = yf.without(this._scenes, scene); + scene.done(); + this.input.remove(scene.keybinds); + this._commandStack.remove(scene.commands); + }, + + DOCUMENT_EVENTS: [ "keydown", "keyup", "visibilitychange" ], + + CANVAS_EVENTS: [ "mousemove", "mousedown", "mouseup" ], + + WINDOW_EVENTS: [ "popstate", "resize", "pageshow", + "yuugamepadbuttondown", "yuugamepadbuttonup" ], + + GESTURES: [ + "touch", "release", "hold", "tap", "doubletap", + "dragstart", "drag", "dragend", "dragleft", "dragright", + "dragup", "dragdown", "swipe", "swipeleft", "swiperight", + "swipeup", "swipedown", "pinch", "pinchin", "pinchout" + ], + + _dispatchSceneInput: function (name, args) { + var scenes = this._scenes; + for (var i = scenes.length - 1; i >= 0; --i) { + var scene = scenes[i]; + var handler = scene.inputs[name]; + if (handler && handler.apply(scene, args)) + return true; + + // FIXME: This may be a heavy ad hoc solution for the + // multiple input layer problems in pwl6. Something + // like this isn't required or allowed for e.g. + // individual keys, why not? + // + // REALLY FIXME: This doesn't even work correctly for + // joystick events, a) because they're prefixed and b) + // because they are in WINDOW_EVENTS so you have to + // manually enumerate them or you also ignore e.g. + // resize. + else if (scene.inputs.consume + && yf.contains(scene.inputs.consume, name)) + return false; + } + return false; + }, + + // Aside from the performance considerations, deferring + // resizing by multiple frames fixes mis-sizing during startup + // and fullscreen transition in node-webkit on Windows. (And + // probably similar bugs in other configurations.) + _doresize: yf.debounce(function () { + this._resized = true; + }, 500), + + _dovisibilitychange: function (event) { + if (event.target.hidden) + this._stopRender(); + else + this._startRender(); + }, + + _dopageshow: function (event) { + if (!history.state) + history.pushState("yuu director", ""); + this._stopRender(); + if (!document.hidden) + this._startRender(); + }, + + _dopopstate: function (event) { + var cmds = []; + if (this._dispatchSceneInput("back", []) + || (cmds = this.input.change("back"))) { + history.pushState("yuu director", ""); + yf.each.call(this, this.execute, cmds); + yuu.stopPropagation(event, true); + } else { + history.back(); + } + }, + + __dogesture: function (event) { + this._updateCaps(event.gesture.srcEvent.type.toLowerCase(), true); + var type = event.type.toLowerCase(); + var p0 = yuu.deviceFromCanvas(event.gesture.startEvent.center); + var p1 = yuu.deviceFromCanvas(event.gesture.center); + if (this._dispatchSceneInput(type, [p0, p1])) + yuu.stopPropagation(event, true); + }, + + // TODO: This munges events, but also, InputState's mousemove + // etc. munge events, in a slightly different but still + // related and fragile way. + // + // Additionally, things run in a Scene handler won't + // affect the InputState's internal state - good for + // avoiding bind execution, bad for consistency. Even if + // a scene handles e.g. "keydown a", input.pressed.a + // should be true. + // + // This is compounded by the lack of actual use cases for any + // of the non-gesture events other than "back" and + // "mousemove". + + _ARGS_FOR: { + keydown: function (event) { + return [yuu.keyEventName(event), {}]; + }, + keyup: function (event) { + return [yuu.keyEventName(event), {}]; + }, + mousemove: function (event) { + return [yuu.deviceFromCanvas(event)]; + }, + mouseup: function (event) { + return [event.button, yuu.deviceFromCanvas(event)]; + }, + mousedown: function (event) { + return [event.button, yuu.deviceFromCanvas(event)]; + }, + gamepadbuttondown: function (event) { + return [event.detail.gamepad, + event.detail.button]; + }, + gamepadbuttonup: function (event) { + return [event.detail.gamepad, event.detail.button]; + }, + }, + + _updateCaps: function (type, definite) { + if (type.startsWith("mouse")) { + if (this._devices.mouse === undefined || definite) + this._devices.mouse = Date.now(); + this._devices.touch = this._devices.touch || false; + } else if (type.startsWith("touch")) { + this._devices.mouse = this._devices.mouse || false; + this._devices.touch = Date.now(); + this._devices.keyboard = this._devices.keyboard || false; + } else if (type.startsWith("key")) { + this._devices.keyboard = Date.now(); + } else if (type.startsWith("gamepad")) { + this._devices.gamepad = Date.now(); + this._devices.touch = this._devices.touch || false; + } + }, + + preferredDevice: function (options) { + options = options || ["keyboard", "touch", "mouse", "gamepad"]; + var devices = this._devices; + var best = yf.foldl(function (best, option) { + var dbest = devices[best]; + var doption = devices[option]; + return dbest === undefined && doption ? option + : doption > dbest ? option : best; + }, options); + for (var i = 0; devices[best] === false && i < options.length; ++i) + if (devices[options[i]] !== false) + best = options[i]; + return best; + }, + + _doevent: function (event) { + var type = event.type.toLowerCase(); + if (type.startsWith("yuu")) + type = type.slice(3); + var args = this._ARGS_FOR[type](event); + var cmds; + this._updateCaps(type, false); + if (this._dispatchSceneInput(type, args)) + yuu.stopPropagation(event, true); + else if ((cmds = this.input[type].apply(this.input, args))) { + var ctx = yf.last(args); + yf.each.call(this, this.execute, cmds, yf.repeat(ctx, cmds.length)); + yuu.stopPropagation(event, true); + } + }, + + _addListener: function (target, name, handler) { + handler = (handler || this["_do" + name] || this._doevent).bind(this); + this._events[name] = { target: target, handler: handler }; + target.addEventListener(name, handler); + }, + + _removeListener: function (name) { + this._events[name].target.removeEventListener( + name, this._events[name].handler); + delete this._events[name]; + }, + + tickHz: { + get: function () { return this._tickHz; }, + set: function (hz) { + this._tickHz = hz; + this._tickCount = 0; + this._timerStart = 0; + } + }, + + currentTime: { get: function () { + return this._timerStart + 1000 * this._tickCount / this._tickHz; + } }, + + currentAudioTime: { get: function () { + /** Audio time of the current tick. + */ + return (this.currentTime + this._audioOffset) / 1000; + } }, + + _startRender: function () { + if (this._rafId !== null) + return; + this._tickCount = 0; + this._timerStart = 0; + // GNU/Linux with node-webkit sizes things incorrectly on + // startup, so force a recalculating as soon as the render + // loop runs. + this._resized = true; + var director = this; + this._rafId = window.requestAnimationFrame(function _ (t) { + if (!director._timerStart) { + director._timerStart = t; + director._audioOffset = yuu.audio + ? yuu.audio.currentTime * 1000 - t + : 0; + } + director._rafId = window.requestAnimationFrame(_); + director.render(t); + }); + }, + + _stopRender: function () { + if (this._rafId !== null) + window.cancelAnimationFrame(this._rafId); + this._rafId = null; + }, + + start: function () { + /** Begin ticking and rendering scenes */ + yf.each(this._addListener.bind(this, window), + this.WINDOW_EVENTS); + yf.each(this._addListener.bind(this, document), + this.DOCUMENT_EVENTS); + yf.each(this._addListener.bind(this, yuu.canvas), + this.CANVAS_EVENTS); + + this._gesture = typeof Hammer !== "undefined" + ? new Hammer(yuu.canvas, { "tap_always": false, + "hold_timeout": 300 }) + : { on: function () {}, off: function () {} }; + this._gesture.on(this.GESTURES.join(" "), this._dogesture); + + // Treat the back button as another kind of input event. Keep + // a token state on the stack to catch the event, and if no + // scene handles it, just go back one more. + // + // Because of browser session restore, state might already be + // on the stack. Throw it out if so. + if (!history.state) + history.pushState("yuu director", ""); + else + history.replaceState("yuu director", ""); + this._startRender(); + }, + + stop: function () { + /** Stop ticking and rendering, clear all scenes */ + this._stopRender(); + yf.eachr(function (scene) { scene.done(); }, this._scenes); + this._scenes = []; + yf.each.call(this, this._removeListener, Object.keys(this._events)); + this._gesture.off(this.GESTURES.join(" "), this._dogesture); + this._gesture = null; + }, + + message: function () { + /** Send a message to all entities/scenes, bottom to top */ + this.entity0.message.apply(this.entity0, arguments); + var scenes = this._scenes; + for (var i = 0; i < scenes.length; ++i) + scenes[i].message.apply(scenes[i], arguments); + }, + + _takeScreenshot: function () { + var date = (new Date()).toLocaleString(); + try { + yuu.downloadURL( + yuu.canvas.toDataURL("image/png"), + document.title + " (" + date + ").png"); + this.toast("\uf030", 0.5, "screenshot"); + } catch (exc) { + var dialog = yuu.showError(exc); + if (dialog) + this.showOverlay(dialog.id); + } + }, + + render: function (t) { + /** Tick and render all scenes, bottom to top */ + var i; + + if (this._resized) { + this._dispatchSceneInput("resize", [yuu.canvas]); + this._resized = false; + } + + t = t - this._timerStart; + var oneTick = 1000.0 / this._tickHz; + while (oneTick * this._tickCount < t) + this.message("tick", oneTick * this._tickCount++, oneTick); + this.message("tock", (t % oneTick) / oneTick); + + yuu.gl.clear(yuu.gl.COLOR_BUFFER_BIT); + var scenes = this._scenes; + var cursor = "default"; + for (i = 0; i < scenes.length; ++i) { + scenes[i].render(); + cursor = scenes[i].cursor || cursor; + } + + if (cursor !== yuu.canvas.style.cursor) + yuu.canvas.style.cursor = cursor; + + for (i = 0; i < this._afterRender.length; ++i) + this._afterRender[i](); + this._afterRender.length = 0; + }, + + toast: yuu.cmd(function (markup, duration, id) { + var toasts = this._toasts; + id = "yuu-toast-" + id; + var toast = id ? document.querySelector("#" + id) : null; + duration = duration || 4; + + if (!toast) { + toast = document.createElement("div"); + toast.id = id; + toast.className = "yuu-toast yuu-fade"; + document.body.appendChild(toast); + } + if (toasts[id]) { + clearTimeout(toasts[id]); + delete toasts[id]; + } + toast.innerHTML = markup; + yuu.afterAnimationFrame(function () { + toast.className = "yuu-toast"; + }); + + var to = setTimeout(function () { + toast.className = "yuu-toast yuu-fade"; + toast.addEventListener("transitionend", function fade () { + toast.removeEventListener("transitionend", fade); + // Stop if the toast was revived between the + // timeout event and transition end, i.e. while it + // was fading out. + if (id && toasts[id] !== to) + return; + toast.className += " yuu-squish"; + toast.addEventListener("transitionend", function squish () { + toast.removeEventListener("transitionend", squish); + if (id && toasts[id] === to) { + delete toasts[id]; + toast.parentNode.removeChild(toast); + } + }); + }); + }, duration * 1000); + if (id) + toasts[id] = to; + }, " ", "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); + + yf.each(function (a) { + a.onclick = function (event) { + yuu.openURL(this.href); + yuu.stopPropagation(event, true); + }; + }, element.querySelectorAll("a[href]:not([yuu-href-internal])")); + + this._director = director; + + element.className = className + " " + this.animation; + element.style.display = "block"; + element.tabIndex = 0; + element.focus(); + element.addEventListener("keydown", this._keydown); + + yuu.afterAnimationFrame(function () { + element.className = className; + }); + }, + + dismiss: yuu.cmd(function () { + var element = this.element; + var className = this.className; + var director = this._director; + var scene = this; + element.className = className + " " + this.animation; + element.addEventListener("transitionend", function _ () { + element.removeEventListener("transitionend", _); + director.removeScene(scene); + }); + }, "", "dismiss this overlay"), + + done: function () { + this.element.style.display = "none"; + this.element.tabIndex = -1; + this.element.className = this.className; + this.element.removeEventListener("keydown", this._keydown); + this._director = null; + yuu.canvas.focus(); + }, + + KEYBINDS: { + "escape": "dismiss" + } + }); + + yuu.registerInitHook(function () { + var elements = document.querySelectorAll("[data-yuu-command]"); + + function handleElement (event) { + /*jshint validthis:true */ + /* `this` comes from being a DOM element event handler. */ + var command = this.getAttribute("data-yuu-command"); + switch (this.tagName.toLowerCase()) { + case "input": + switch (this.type.toLowerCase()) { + case "range": + command += " " + this.value; + break; + case "checkbox": + command += " " + (this.checked ? "1" : "0"); + break; + } + break; + } + yuu.director.execute(command); + yuu.stopPropagation(event); + } + + yf.each(function (element) { + switch (element.tagName.toLowerCase()) { + case "input": + switch (element.type.toLowerCase()) { + case "range": + element.oninput = handleElement; + element.onchange = handleElement; + break; + case "checkbox": + element.onchange = handleElement; + break; + } + break; + case "div": + case "span": + element.onclick = handleElement; + element.onkeydown = function (event) { + var name = yuu.keyEventName(event); + if (name === "space" || name === "return") + handleElement.call(this, event); + }; + break; + default: + element.onclick = handleElement; + break; + } + }, elements); + + yuu.defaultKeybinds.bind("control+`", "showDevTools"); + yuu.defaultKeybinds.bind("f11", "++fullscreen"); + yuu.defaultKeybinds.bind("f12", "screenshot"); + yuu.defaultKeybinds.bind( + "control+s", "++mute && toast \uf026 1 mute || toast \uf028 1 mute"); + + var director = yuu.director = new yuu.Director(); + /** The standard director */ + + yuu.registerInitHook(function () { + return yuu.ready(director.scenes); + }); + }); + +}).call(typeof exports === "undefined" ? this : exports, + typeof exports === "undefined" + ? this.yuu : (module.exports = require('./core'))); diff --git a/src/yuu/gfx.js b/src/yuu/gfx.js new file mode 100644 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): + if not os.path.isdir(appdir): + raise StandardError("input (%r) is not a directory" % appdir) + indexes = [] + icons = [] + for root, dirnames, filenames in os.walk(appdir): + root = os.path.relpath(root, appdir) + for filename in filenames: + if filename.lower() == "index.html": + indexes.append(os.path.join(root, filename)) + if ("icon" in filename.lower() + and filename.lower().endswith((".ico", ".png"))): + icons.append(os.path.join(root, filename)) + + indexes.sort(key=lambda fn: (fn.count("/"), fn)) + icons.sort(key=lambda fn: (fn.count("/"), fn)) + name = os.path.basename(appdir).split(".")[0] + package = { + "main": indexes[0], + "name": name, + "version": "0.0.0", + "window": { + "show": False, + "toolbar": False, + "title": name, + "min_width": 300, + "min_height": 200, + } + } + + if icons: + package["window"]["icon"] = icons[0] + + with open(os.path.join(appdir, indexes[0]), "r") as fobj: + header = fobj.read(4096) + try: + title = re.search("([^<]+)<", header).groups()[0] + except AttributeError: + print >>sys.stderr, "No <title> found in %r." % fobj.name + else: + package["window"]["title"] = title + try: + version = re.search(attr("version"), header).groups()[0] + except AttributeError: + print >>sys.stderr, "No version found in %r." % fobj.name + else: + package["version"] = version + try: + appid = re.search(attr("appid"), header).groups()[0] + except AttributeError: + print >>sys.stderr, "No appid found in %r." % fobj.name + else: + package["name"] = appid + + if "+" not in package["version"]: + package["version"] += "+" + package["version"] += time.strftime("%Y%m%d%H%M%S") + + with open(os.path.join(appdir, "package.json"), "w") as fobj: + json.dump(package, fobj) + +if __name__ == "__main__": + import sys + try: + appdir = sys.argv[1] + except IndexError: + raise SystemExit("Usage: %s appdir" %( + sys.argv[0])) + else: + main(appdir) + diff --git a/tools/generate-osx-app b/tools/generate-osx-app new file mode 100755 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..d13e39f --- /dev/null +++ b/tools/nw-linux-wrapper @@ -0,0 +1,25 @@ +#!/bin/sh + +# https://github.com/rogerwang/node-webkit/issues/136 +# https://github.com/rogerwang/node-webkit/wiki/The-solution-of-lacking-libudev.so.0 +# +# Though that page would be more accurately titled "apparently even +# Intel, Google, and every commercial distribution can't be bothered +# to maintain trivial compatibility on desktop Linux." + +SELF=$(readlink -f "$0") +NWDIR=$(dirname "$SELF")/nw +NWBIN=$(command -v "$NWDIR/nw" nw | head -n 1) +PACKAGE="$NWDIR/package.nw" + +if [ ! -x "$NWBIN" ]; then + echo "node-webkit (nw) executable could not be found." >&2 + exit 127 +fi + +"$NWBIN" "$@" "$PACKAGE" +if [ "$?" = "127" -a -w "$NWBIN" ]; then + echo "Applying libudev.so.1 hack." >&2 + sed -i 's/udev\.so\.0/udev.so.1/g' "$NWBIN" + "$NWBIN" "$@" "$PACKAGE" +fi diff --git a/tools/rcedit.exe b/tools/rcedit.exe new file mode 100644 index 0000000..9a8cb71 Binary files /dev/null and b/tools/rcedit.exe differ