diff --git a/community/amidakuji/LICENSE b/community/amidakuji/LICENSE new file mode 100644 index 0000000..ab60297 --- /dev/null +++ b/community/amidakuji/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +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/community/amidakuji/Makefile b/community/amidakuji/Makefile new file mode 100644 index 0000000..e9fa78e --- /dev/null +++ b/community/amidakuji/Makefile @@ -0,0 +1,45 @@ +OUT := amidakuji +ASSET_TARGET := glossary/asset.go +ASSET_SOURCE_DIR := assets +VERSION := $(shell git describe --always --long) +PKG_LIST := $(shell go list ./... | grep -v /vendor/) +GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/) + +all: build build_windows + +${ASSET_TARGET}: + go-bindata -o "${ASSET_TARGET}" -pkg "glossary" -prefix "${ASSET_SOURCE_DIR}" ${ASSET_SOURCE_DIR}/emoji ${ASSET_SOURCE_DIR}/karaoke ${ASSET_SOURCE_DIR}/NanumBarunGothic.ttf + +build: ${ASSET_TARGET} + go build -i -v -o ${OUT} -ldflags "-w -s -X main.version=${VERSION}" + +build_windows: ${ASSET_TARGET} + go build -i -v -o ${OUT}.exe -ldflags "-w -s -X main.version=${VERSION} -H windowsgui" + +run: build + ./${OUT} + +test: + @go test -short ${PKG_LIST} + +vet: + @go vet -copylocks=false ${PKG_LIST} + +vet_annoying: + @go vet ${PKG_LIST} + +lint: + @for file in ${GO_FILES} ; do \ + golint $$file ; \ + done + +#static: vet lint +# go build -i -v -o ${OUT}-${VERSION} -ldflags "-extldflags \"-static\" -w -s -X main.version=${VERSION}" + +#static_windows: vet lint +# go build -i -v -o ${OUT}-${VERSION}.exe -ldflags "-extldflags \"-static\" -w -s -X main.version=${VERSION} -H windowsgui" + +clean: + -@rm ${ASSET_TARGET} ${OUT} ${OUT}.exe #${OUT}-* + +.PHONY: build build_windows run vet vet_annoying lint clean diff --git a/community/amidakuji/README.md b/community/amidakuji/README.md new file mode 100644 index 0000000..c546e33 --- /dev/null +++ b/community/amidakuji/README.md @@ -0,0 +1,93 @@ +# [AMIDA KUJI](https://github.com/NaniteFactory/amidakuji/tree/1bf57c3639e4e5628d96d9171ed9e679b658fadb) + +> Ghost Leg (Chinese: 畫鬼腳), known in Japan as Amidakuji (阿弥陀籤, "Amida lottery", so named because the paper was folded into a fan shape resembling Amida's halo) or in Korea as Sadaritagi (사다리타기, literally "ladder climbing"), is a method of lottery designed to create random pairings between two sets of any number of things, as long as the number of elements in each set is the same. This is often used to distribute things among people, where the number of things distributed is the same as the number of people. For instance, chores or prizes could be assigned fairly and randomly this way. + +(Explanation from [Wikipedia](https://en.wikipedia.org/wiki/Ghost_Leg)) + +- - - + +## Examples + +| [GIF1](examples/user_conf_sample6.json) | [GIF2](examples/user_conf_sample3.json) | +| --- | --- | +| ![1](examples/1.gif) | ![2](examples/2.gif) | + +| GIF3 | +| --- | +| ![3](examples/3.gif) | + +- - - + +## Control + +| | Action | Input | +| --- | --- | --- | +|🔀 | Shuffle | 1 | +| ▶️ | Find path | 2 | +| ⏩ | Find path (faster) | 3 | +| ⏸ | Pause | Space | +| ⬆️➡️⬇️⬅️ | Move camera around | Arrow keys | +| ↩️ | Rotate camera | Enter | +| 🔭 | Zoom in and out | Scroll | +| 🎇 | Firework | Left click | +| 🔬 | Inspection | Right click | +| 🔁 | Toggle full screen mode | Tab | + +- - - + +## Build + +#### Windows + +``` +$ go get -v github.com/faiface/pixel/examples/community/amidakuji/... +$ go get -v -u github.com/go-bindata/go-bindata/... +``` + +``` +$ cd $GOPATH/src/github.com/faiface/pixel/examples/community/amidakuji/ +$ make +``` + +#### Ubuntu + +``` +$ sudo apt-get clean +$ sudo rm -r /var/lib/apt/lists/* +$ sudo apt update +``` + +``` +$ sudo apt install libglib2.0-dev libpango1.0-dev libasound2-dev libgdk-pixbuf2.0-dev libgl1-mesa-dev xorg-dev libgtk2.0-dev +``` + +``` +$ go get -v github.com/faiface/pixel/examples/community/amidakuji/... +$ go get -v -u github.com/go-bindata/go-bindata/... +``` + +``` +$ cd $GOPATH/src/github.com/faiface/pixel/examples/community/amidakuji/ +$ make +``` + +- - - + +## External sources + +#### Library +- [Pixel](https://github.com/faiface/pixel/tree/7cff3ce3aed80129b7b1dd57e63439426e11b6ee) +- [Beep](https://github.com/faiface/beep/tree/63cc6fbbac46dba1a03e55f0ebc965d6c82ca8e1) +- [GLFW 3.2](https://github.com/go-gl/glfw/tree/513e4f2bf85c31fba0fc4907abd7895242ccbe50/v3.2/glfw) +- [dialog](https://github.com/sqweek/dialog/tree/2f9d9e5dd848a3bad4bdd0210c73bb90c13a3791) + +#### Music +- [Night Tempo - Pure Present](https://nighttempo.bandcamp.com/album/pure-present) - [08 Kikuchi Momoko - Night Cruising (Night Tempo 100% Pure Remastered)](https://nighttempo.bandcamp.com/track/kikuchi-momoko-night-cruising-night-tempo-100-pure-remastered-2) +- [Night Tempo - Pure Present](https://nighttempo.bandcamp.com/album/pure-present) - [19 Takeuchi Mariya - Plastic Love (Night Tempo 100% Pure Remastered)](https://nighttempo.bandcamp.com/track/takeuchi-mariya-plastic-love-night-tempo-100-pure-remastered-3) + +#### Image +- [Gophers...](https://github.com/egonelbre/gophers/tree/dfb1bc3e6092179bd80d2e4156a8d32dba484cc9) + +#### Font +- [나눔바른고딕 (NanumBarunGothic.ttf)](https://hangeul.naver.com/2017/nanum) + diff --git a/community/amidakuji/assets/NanumBarunGothic.ttf b/community/amidakuji/assets/NanumBarunGothic.ttf new file mode 100644 index 0000000..c314868 Binary files /dev/null and b/community/amidakuji/assets/NanumBarunGothic.ttf differ diff --git a/community/amidakuji/assets/emoji/gopher-angry.png b/community/amidakuji/assets/emoji/gopher-angry.png new file mode 100644 index 0000000..6d208ea Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-angry.png differ diff --git a/community/amidakuji/assets/emoji/gopher-at-peace.png b/community/amidakuji/assets/emoji/gopher-at-peace.png new file mode 100644 index 0000000..bc4de32 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-at-peace.png differ diff --git a/community/amidakuji/assets/emoji/gopher-blushing.png b/community/amidakuji/assets/emoji/gopher-blushing.png new file mode 100644 index 0000000..4d4023a Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-blushing.png differ diff --git a/community/amidakuji/assets/emoji/gopher-cold-sweat.png b/community/amidakuji/assets/emoji/gopher-cold-sweat.png new file mode 100644 index 0000000..1411197 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-cold-sweat.png differ diff --git a/community/amidakuji/assets/emoji/gopher-confused.png b/community/amidakuji/assets/emoji/gopher-confused.png new file mode 100644 index 0000000..d60f060 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-confused.png differ diff --git a/community/amidakuji/assets/emoji/gopher-crying-river.png b/community/amidakuji/assets/emoji/gopher-crying-river.png new file mode 100644 index 0000000..e689296 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-crying-river.png differ diff --git a/community/amidakuji/assets/emoji/gopher-crying.png b/community/amidakuji/assets/emoji/gopher-crying.png new file mode 100644 index 0000000..2fc197c Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-crying.png differ diff --git a/community/amidakuji/assets/emoji/gopher-dead.png b/community/amidakuji/assets/emoji/gopher-dead.png new file mode 100644 index 0000000..98e1022 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-dead.png differ diff --git a/community/amidakuji/assets/emoji/gopher-expressionless.png b/community/amidakuji/assets/emoji/gopher-expressionless.png new file mode 100644 index 0000000..e4b14ce Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-expressionless.png differ diff --git a/community/amidakuji/assets/emoji/gopher-eyeroll.gif b/community/amidakuji/assets/emoji/gopher-eyeroll.gif new file mode 100644 index 0000000..f05c48a Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-eyeroll.gif differ diff --git a/community/amidakuji/assets/emoji/gopher-facepalm.png b/community/amidakuji/assets/emoji/gopher-facepalm.png new file mode 100644 index 0000000..5ae88cc Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-facepalm.png differ diff --git a/community/amidakuji/assets/emoji/gopher-happy.png b/community/amidakuji/assets/emoji/gopher-happy.png new file mode 100644 index 0000000..c862cd7 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-happy.png differ diff --git a/community/amidakuji/assets/emoji/gopher-heart-eyes.png b/community/amidakuji/assets/emoji/gopher-heart-eyes.png new file mode 100644 index 0000000..32409c0 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-heart-eyes.png differ diff --git a/community/amidakuji/assets/emoji/gopher-heart.png b/community/amidakuji/assets/emoji/gopher-heart.png new file mode 100644 index 0000000..18c1aec Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-heart.png differ diff --git a/community/amidakuji/assets/emoji/gopher-idea.png b/community/amidakuji/assets/emoji/gopher-idea.png new file mode 100644 index 0000000..a84a5e8 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-idea.png differ diff --git a/community/amidakuji/assets/emoji/gopher-insomnia.png b/community/amidakuji/assets/emoji/gopher-insomnia.png new file mode 100644 index 0000000..0e8fee3 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-insomnia.png differ diff --git a/community/amidakuji/assets/emoji/gopher-mind-blown.png b/community/amidakuji/assets/emoji/gopher-mind-blown.png new file mode 100644 index 0000000..54c314f Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-mind-blown.png differ diff --git a/community/amidakuji/assets/emoji/gopher-neutral.png b/community/amidakuji/assets/emoji/gopher-neutral.png new file mode 100644 index 0000000..9b88592 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-neutral.png differ diff --git a/community/amidakuji/assets/emoji/gopher-no-peeking.png b/community/amidakuji/assets/emoji/gopher-no-peeking.png new file mode 100644 index 0000000..035011c Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-no-peeking.png differ diff --git a/community/amidakuji/assets/emoji/gopher-not-sure-if.png b/community/amidakuji/assets/emoji/gopher-not-sure-if.png new file mode 100644 index 0000000..afe91cb Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-not-sure-if.png differ diff --git a/community/amidakuji/assets/emoji/gopher-pirate.png b/community/amidakuji/assets/emoji/gopher-pirate.png new file mode 100644 index 0000000..36cfede Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-pirate.png differ diff --git a/community/amidakuji/assets/emoji/gopher-sad-sweat.png b/community/amidakuji/assets/emoji/gopher-sad-sweat.png new file mode 100644 index 0000000..afd2a62 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-sad-sweat.png differ diff --git a/community/amidakuji/assets/emoji/gopher-sad.png b/community/amidakuji/assets/emoji/gopher-sad.png new file mode 100644 index 0000000..14545eb Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-sad.png differ diff --git a/community/amidakuji/assets/emoji/gopher-sick.png b/community/amidakuji/assets/emoji/gopher-sick.png new file mode 100644 index 0000000..b1218a6 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-sick.png differ diff --git a/community/amidakuji/assets/emoji/gopher-sleeping.png b/community/amidakuji/assets/emoji/gopher-sleeping.png new file mode 100644 index 0000000..934fc9e Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-sleeping.png differ diff --git a/community/amidakuji/assets/emoji/gopher-sleepy.png b/community/amidakuji/assets/emoji/gopher-sleepy.png new file mode 100644 index 0000000..784eeae Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-sleepy.png differ diff --git a/community/amidakuji/assets/emoji/gopher-smiling-blushing.png b/community/amidakuji/assets/emoji/gopher-smiling-blushing.png new file mode 100644 index 0000000..9d9b07a Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-smiling-blushing.png differ diff --git a/community/amidakuji/assets/emoji/gopher-smiling-sweat.png b/community/amidakuji/assets/emoji/gopher-smiling-sweat.png new file mode 100644 index 0000000..305b6af Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-smiling-sweat.png differ diff --git a/community/amidakuji/assets/emoji/gopher-smiling.png b/community/amidakuji/assets/emoji/gopher-smiling.png new file mode 100644 index 0000000..07b8d70 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-smiling.png differ diff --git a/community/amidakuji/assets/emoji/gopher-thinking.png b/community/amidakuji/assets/emoji/gopher-thinking.png new file mode 100644 index 0000000..482d02b Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-thinking.png differ diff --git a/community/amidakuji/assets/emoji/gopher-tired.png b/community/amidakuji/assets/emoji/gopher-tired.png new file mode 100644 index 0000000..bf886b5 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-tired.png differ diff --git a/community/amidakuji/assets/emoji/gopher-trying-hard.png b/community/amidakuji/assets/emoji/gopher-trying-hard.png new file mode 100644 index 0000000..264b2e1 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-trying-hard.png differ diff --git a/community/amidakuji/assets/emoji/gopher-victorious.png b/community/amidakuji/assets/emoji/gopher-victorious.png new file mode 100644 index 0000000..e9ac176 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-victorious.png differ diff --git a/community/amidakuji/assets/emoji/gopher-wink.png b/community/amidakuji/assets/emoji/gopher-wink.png new file mode 100644 index 0000000..887d510 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-wink.png differ diff --git a/community/amidakuji/assets/emoji/gopher-wondering.png b/community/amidakuji/assets/emoji/gopher-wondering.png new file mode 100644 index 0000000..c6b47d5 Binary files /dev/null and b/community/amidakuji/assets/emoji/gopher-wondering.png differ diff --git a/community/amidakuji/assets/karaoke/kikuchimomoko-nightcruising.ogg b/community/amidakuji/assets/karaoke/kikuchimomoko-nightcruising.ogg new file mode 100644 index 0000000..b803a4f Binary files /dev/null and b/community/amidakuji/assets/karaoke/kikuchimomoko-nightcruising.ogg differ diff --git a/community/amidakuji/assets/karaoke/takeuchimariya-plasticlove.ogg b/community/amidakuji/assets/karaoke/takeuchimariya-plasticlove.ogg new file mode 100644 index 0000000..8cec80b Binary files /dev/null and b/community/amidakuji/assets/karaoke/takeuchimariya-plasticlove.ogg differ diff --git a/community/amidakuji/examples/1.gif b/community/amidakuji/examples/1.gif new file mode 100644 index 0000000..69fc08f Binary files /dev/null and b/community/amidakuji/examples/1.gif differ diff --git a/community/amidakuji/examples/2.gif b/community/amidakuji/examples/2.gif new file mode 100644 index 0000000..b4b8b7e Binary files /dev/null and b/community/amidakuji/examples/2.gif differ diff --git a/community/amidakuji/examples/3.gif b/community/amidakuji/examples/3.gif new file mode 100644 index 0000000..93cfa5e Binary files /dev/null and b/community/amidakuji/examples/3.gif differ diff --git a/community/amidakuji/examples/user_conf_sample1.json b/community/amidakuji/examples/user_conf_sample1.json new file mode 100644 index 0000000..b1b8b3d --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample1.json @@ -0,0 +1,21 @@ +{ + "window_width": 600, + "window_height": 900, + + "max_player": 4, + "max_level": 80, + + "width": 1500, + "height": 1000, + "zoom": -3, + "rotate_degree": -90, + + "margin_top": 50, + "margin_right": 100, + "margin_bottom": 50, + "margin_left": 150, + + "font_size": 20, + "picks": ["Bulbasaur","Ivysaur","Venusaur","Charmander","Charmeleon","Charizard","Squirtle","Wartortle","Blastoise","Caterpie","Metapod","Butterfree","Weedle","Kakuna","Beedrill","Pidgey","Pidgeotto","Pidgeot","Rattata","Rattata"], + "prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"] +} diff --git a/community/amidakuji/examples/user_conf_sample2.json b/community/amidakuji/examples/user_conf_sample2.json new file mode 100644 index 0000000..29a0158 --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample2.json @@ -0,0 +1,21 @@ +{ + "window_width": 1200, + "window_height": 800, + + "max_player": 5, + "max_level": 100, + + "width": 1500, + "height": 1000, + "zoom": -2, + "rotate_degree": -360, + + "margin_top": 50, + "margin_right": 100, + "margin_bottom": 50, + "margin_left": 150, + + "font_size": 20, + "picks": ["Psyduck","Golduck","Mankey","Primeape","Growlithe"], + "prizes": ["None","Potion","Antidote","Burn Heal","Ice Heal","Awakening","Paralyze Heal","Full Restore","Max Potion","Hyper Potion","Super Potion","Full Heal","Revive","Max Revive","Fresh Water","Soda Pop","Lemonade","Moomoo Milk"] +} diff --git a/community/amidakuji/examples/user_conf_sample3.json b/community/amidakuji/examples/user_conf_sample3.json new file mode 100644 index 0000000..bb0f23e --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample3.json @@ -0,0 +1,21 @@ +{ + "window_width": 1800, + "window_height": 600, + + "max_player": 50, + "max_level": 2000, + + "width": 3000, + "height": 1000, + "zoom": -3, + "rotate_degree": -360, + + "margin_top": 50, + "margin_right": 50, + "margin_bottom": 50, + "margin_left": 100, + + "font_size": 20, + "picks": [], + "prizes": [] +} diff --git a/community/amidakuji/examples/user_conf_sample4.json b/community/amidakuji/examples/user_conf_sample4.json new file mode 100644 index 0000000..d448b09 --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample4.json @@ -0,0 +1,21 @@ +{ + "window_width": 1800, + "window_height": 600, + + "max_player": 8, + "max_level": 300, + + "width": 3000, + "height": 1000, + "zoom": -3, + "rotate_degree": -720, + + "margin_top": 50, + "margin_right": 50, + "margin_bottom": 50, + "margin_left": 100, + + "font_size": 20, + "picks": ["피카츄", "꼬부기", "깨비참", "성원숭", "파이리", "날쌩마", "뚜벅쵸", "케이시", "망나뇽", "잠만보", "파라스", "덩쿠리", "뿔카노", "버터플", "꼬마돌", "ㅁ", "ㄴ", "ㅇ", "ㄹ", "a", "s", "d", "f"], + "prizes": ["꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ"] +} diff --git a/community/amidakuji/examples/user_conf_sample5.json b/community/amidakuji/examples/user_conf_sample5.json new file mode 100644 index 0000000..dd9a0a2 --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample5.json @@ -0,0 +1,21 @@ +{ + "window_width": 1800, + "window_height": 600, + + "max_player": 8, + "max_level": 300, + + "width": 3000, + "height": 1000, + "zoom": -3, + "rotate_degree": -360, + + "margin_top": 50, + "margin_right": 120, + "margin_bottom": 50, + "margin_left": 150, + + "font_size": 18, + "picks": ["Tauros", "Magikarp", "Gyarados", "Lapras", "Ditto", "Eevee", "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte"], + "prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"] +} diff --git a/community/amidakuji/examples/user_conf_sample6.json b/community/amidakuji/examples/user_conf_sample6.json new file mode 100644 index 0000000..bbb576c --- /dev/null +++ b/community/amidakuji/examples/user_conf_sample6.json @@ -0,0 +1,21 @@ +{ + "window_width": 800.0, + "window_height": 800.0, + + "max_player": 3.0, + "max_level": 20.0, + + "width": 500.0, + "height": 500.0, + "zoom": 1.0, + "rotate_degree": 270.0, + + "margin_top": 50.0, + "margin_right": 100.0, + "margin_bottom": 50.0, + "margin_left": 120.0, + + "font_size": 28.0, + "picks": ["Tauros", "Magikarp", "Eevee", "Lapras", "Ditto", "Gyarados", "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte"], + "prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"] +} diff --git a/community/amidakuji/game.go b/community/amidakuji/game.go new file mode 100644 index 0000000..466ba69 --- /dev/null +++ b/community/amidakuji/game.go @@ -0,0 +1,774 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "math" + "math/rand" + "os" + "reflect" + "strings" + "sync" + "time" + "unsafe" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + "github.com/faiface/pixel/examples/community/amidakuji/glossary/jukebox" + glfw "github.com/go-gl/glfw/v3.2/glfw" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/pixelgl" + "github.com/faiface/pixel/text" + "github.com/sqweek/dialog" + "golang.org/x/image/colornames" +) + +// Actor updates and draws itself. It acts as a game object. +type Actor interface { + Drawer + Updater +} + +// Drawer draws itself. +type Drawer interface { + Draw() +} + +// Updater updates itself. +type Updater interface { + Update() +} + +// ------------------------------------------------------------------------- +// Core game + +// game is a path finder. +// Also it manages and draws everything about... +type game struct { + // something system, somthing runtime + window *pixelgl.Window // lazy init + bg pixel.RGBA + camera *gg.Camera // lazy init + fpsw *gg.FPSWatch + dtw gg.DtWatch + vsync <-chan time.Time // lazy init + // game state + isRefreshedLadder bool + isRefreshedNametags bool + isScalpelMode bool + // drawings + mutex sync.Mutex // It is unsafe to access any refd; ptrd object without a critical section. + nPlayers int + ladder *Ladder + scalpel *Scalpel + paths []Path + emojis []pixel.Sprite + nametagPicks Nametags + nametagPrizes Nametags + atlas *text.Atlas + galaxy *gg.Galaxy + explosions *gg.Explosions + // other user settings + fontSize float64 + winWidth float64 // The screen width, not the game width. + winHeight float64 + initialZoomLevel float64 + initialRotateDegree float64 +} + +type gameConfig struct { + nParticipants int + nLevel int + winWidth float64 + winHeight float64 + width float64 + height float64 + initialZoomLevel float64 + initialRotateDegree float64 + paddingTop float64 + paddingRight float64 + paddingBottom float64 + paddingLeft float64 + fontSize float64 + nametagPicks []string + nametagPrizes []string +} + +// init game +func newGame(cfg gameConfig) *game { + + newEmojis := func(nParticipants int) (emojis []pixel.Sprite) { + emojis = make([]pixel.Sprite, nParticipants) + const dir = "emoji" + randomNames, err := gg.AssetDir(dir) // The order is random because they're from a map. + if err != nil { + return nil + } + nRandomNames := len(randomNames) + for participant := 0; participant < nParticipants; participant++ { + emojis[participant] = *gg.NewSprite(dir + "/" + randomNames[participant%nRandomNames]) // val, not ptr + } + return emojis + } + + g := game{ + bg: gg.RandomNiceColor(), + fpsw: gg.NewFPSWatchSimple(pixel.V(cfg.winWidth, cfg.winHeight), gg.Top, gg.Right), + isRefreshedLadder: false, + isRefreshedNametags: false, + isScalpelMode: false, + nPlayers: cfg.nParticipants, + ladder: NewLadder( + cfg.nParticipants, cfg.nLevel, + cfg.width, cfg.height, + cfg.paddingTop, cfg.paddingRight, + cfg.paddingBottom, cfg.paddingLeft, + ), + scalpel: &Scalpel{}, + paths: make([]Path, cfg.nParticipants), + emojis: newEmojis(cfg.nParticipants), + nametagPicks: make([]Nametag, cfg.nParticipants), // val, not ptr + nametagPrizes: make([]Nametag, cfg.nParticipants), // val, not ptr + atlas: gg.NewAtlas( + "", cfg.fontSize, + []rune(strings.Join(cfg.nametagPicks, "")+strings.Join(cfg.nametagPrizes, "")), + ), // A prepared set of images of characters or symbols to be drawn. + galaxy: gg.NewGalaxy(cfg.width, cfg.height, 400), + explosions: gg.NewExplosions(cfg.width, cfg.width, nil, 5), + initialZoomLevel: cfg.initialZoomLevel, + initialRotateDegree: cfg.initialRotateDegree, + winWidth: cfg.winWidth, + winHeight: cfg.winHeight, + } + + // init paths + g.ResetPaths() + + // copy nametags + copyNametagPicks := func(dstNametags []Nametag, srcNames []string) { + positions := g.ladder.PtsAtLevelOfPicks() + for i := 0; i < cfg.nParticipants; i++ { + posAdjust := positions[i] + posAdjust.Y += 5 + posAdjust.X -= 60 + dstNametags[i] = *NewNametagSimple( + g.atlas, "", posAdjust, + gg.Middle, gg.Right, + ) // val, not ptr + if i < len(srcNames) { + dstNametags[i].desc = srcNames[i] + } + } + } + copyNametagPrizes := func(dstNametags []Nametag, srcNames []string) { + positions := g.ladder.PtsAtLevelOfPrizes() + for i := 0; i < cfg.nParticipants; i++ { + posAdjust := positions[i] + posAdjust.Y += 5 + posAdjust.X += 15 + dstNametags[i] = *NewNametagSimple( + g.atlas, "", posAdjust, + gg.Middle, gg.Left, + ) // val, not ptr + if i < len(srcNames) { + dstNametags[i].desc = srcNames[i] + } + } + } + copyNametagPicks(g.nametagPicks, cfg.nametagPicks) + copyNametagPrizes(g.nametagPrizes, cfg.nametagPrizes) + // log.Println(g.nametagPicks[1].desc) // + + return &g +} + +func (g *game) Draw() { + g.mutex.Lock() + defer g.mutex.Unlock() + + // This was originally an argument of this function. + var t pixel.BasicTarget + t = g.window + + // --------------------------------------------------- + // 1. canvas a game world + t.SetMatrix(g.camera.Transform()) + + // Draw()s in an order. + g.galaxy.Draw(t) + g.ladder.Draw(t) + for iPath := range g.paths { + g.paths[iPath].Draw(t) + } + g.nametagPicks.Draw(t) + g.nametagPrizes.Draw(t) + if g.explosions.IsExploding() { + g.explosions.Draw(t) + } + for iEmoji := range g.emojis { + g.emojis[iEmoji].Draw( + t, pixel.IM. + Scaled(pixel.ZV, 2). + Rotated(pixel.ZV, -g.camera.Angle()). + Moved(g.paths[iEmoji].PosTip()), + ) + } + if g.isScalpelMode { + g.scalpel.Draw(t) + } + if g.isScalpelMode { + UpdateDrawUnprojekt(g.window, g.ladder.bound, colornames.Blue, g.camera.Transform()) + UpdateDrawUnprojekt2(g.window, g.ladder.bound, colornames.Red, *g.camera) + } + + // --------------------------------------------------- + // 2. canvas a screen + t.SetMatrix(pixel.IM) + + // Draw()s in an order. + g.fpsw.Draw(g.window) + if g.isScalpelMode { + UpdateDrawProjekt(g.window, g.ladder.bound, colornames.Black, g.camera.Transform()) + } +} + +func (g *game) Update(dt float64) { + g.mutex.Lock() + defer g.mutex.Unlock() + + // The camera would and should update every frame. + g.camera.Update(dt) + + // Update only if there is a need. + // isRefreshedLadder be set to false if there was an update to the ladder or its scalpel. + if !g.isRefreshedLadder { + g.ladder.Update() + g.scalpel.Update(*g.ladder) + g.isRefreshedLadder = true + } + + // Only update when there is a need. + if !g.isRefreshedNametags { + g.nametagPicks.Update() + g.nametagPrizes.Update() + g.isRefreshedNametags = true + } + + // Only currently animating paths need to update each frame. + for iPath := range g.paths { + if g.paths[iPath].IsAnimating() { + g.paths[iPath].Update(g.ladder.colors[iPath]) + } + } + + // As long as it doesn't hurt the framerate. + if g.fpsw.GetFPS() >= 10 { + g.galaxy.Update(dt) + } + + // Only update when there is at least one (animating) explosion. + if g.explosions.IsExploding() { + g.explosions.Update(dt) + } +} + +func (g *game) OnResize(width, height float64) { + g.camera.SetScreenBound(pixel.R(0, 0, width, height)) + g.fpsw.SetPos(pixel.V(width, height), gg.Top, gg.Right) + // g.explosions.SetBound(width, height) +} + +// ------------------------------------------------------------------------- +// Single path + +// ClearPath of a participant. +func (g *game) ClearPath(participant int) { + g.paths[participant] = *NewPathEmpty() +} + +// ResetPath of a participant. +func (g *game) ResetPath(participant int) { + // GeneratePath contains a path-finding algorithm. This function is used as a path finder. + GeneratePath := func(g *game, participant int) Path { + const icol int = 0 // level + irow := participant // participant + grid := g.ladder.grid + route := []pixel.Vec{} + prize := -1 + for level := icol; level < g.ladder.nLevel; level++ { + route = append(route, grid[irow][level]) + prize = irow + if irow+1 < g.ladder.nParticipants { + if g.ladder.bridges[irow][level] { + irow++ // cross the bridge ... to the left (south) + route = append(route, grid[irow][level]) + prize = irow + continue + } + } + if irow-1 >= 0 { + if g.ladder.bridges[irow-1][level] { + irow-- // cross the bridge ... to the right (north) + route = append(route, grid[irow][level]) + prize = irow + continue + } + } + } + // log.Println(participant, prize, irow) // + + // A path found here is called a route or roads. + return *NewPath(route, &prize) // val, not ptr + } + + g.paths[participant] = GeneratePath(g, participant) // path-find + g.paths[participant].OnPassedEachPoint = func(pt pixel.Vec, dir pixel.Vec) { + g.explosions.ExplodeAt(pt, dir.Scaled(2)) + } +} + +func (g *game) AnimatePath(participant int) { + g.paths[participant].Animate() +} + +func (g *game) AnimatePathInTime(participant int, sec float64) { + g.paths[participant].AnimateInTime(sec) +} + +// ------------------------------------------------------------------------- +// All paths + +func (g *game) ResetPaths() { + g.mutex.Lock() + defer g.mutex.Unlock() + + for participant := 0; participant < g.nPlayers; participant++ { + g.ResetPath(participant) + } +} + +// AnimatePaths in order. +func (g *game) AnimatePaths(thunkAnimatePath func(participant int)) { + g.mutex.Lock() + defer g.mutex.Unlock() + + for participant := 0; participant < g.nPlayers; participant++ { + participantCurr := participant + participantNext := participant + 1 + prize := g.paths[participantCurr].GetPrize() + title := "Result" + caption := fmt.Sprint( + " 👆 Pick\t(No. ", participantCurr+1, ")\t", g.nametagPicks[participantCurr], "\t", + "\r\n", "\r\n", + " 🎁 Prize\t(No. ", prize+1, ")\t", g.nametagPrizes[prize], "\t", + "\r\n", + ) + g.paths[participant].OnFinishedAnimation = func() { + if g.window.Monitor() == nil { + dialog.Message("%s", caption).Title(title).Info() + } + if participantNext < g.nPlayers { + thunkAnimatePath(participantNext) + } + g.paths[participantCurr].OnFinishedAnimation = nil + } + } + thunkAnimatePath(0) +} + +// ------------------------------------------------------------------------- +// Game controls + +func (g *game) Reset() { + g.ladder.Reset() + g.ResetPaths() + g.isRefreshedLadder = false +} + +// Shuffle in an approximate time. +func (g *game) Shuffle(times, inMillisecond int) { + speed := g.galaxy.Speed() + g.galaxy.SetSpeed(speed * 10) + { + i := 0 + for range time.Tick( + (time.Millisecond * time.Duration(inMillisecond)) / time.Duration(times), + ) { + g.bg = gg.RandomNiceColor() + g.Reset() + i++ + if i >= times { + break + } + } + } + g.galaxy.SetSpeed(speed) +} + +// Pause the game. +func (g *game) Pause() { + for i := range g.paths { + if g.paths[i].IsAnimating() { + g.paths[i].Pause() + } + } +} + +// Resume after pause. +func (g *game) Resume() { + g.dtw.Dt() + for i := range g.paths { + if g.paths[i].IsAnimating() { + g.paths[i].Resume() + } + } +} + +func (g *game) SetFullScreenMode(on bool) { + if on { + monitor := pixelgl.PrimaryMonitor() + width, height := monitor.Size() + // log.Println(monitor.VideoModes()) // + g.window.SetMonitor(monitor) + go func(width, height float64) { + g.OnResize(width, height) + }(width, height) + } else if !on { // off + g.window.SetMonitor(nil) + } else { + panic(errors.New("it may be thread")) + } +} + +// ------------------------------------------------------------------------- +// Read only methods + +// WindowDeep is a hacky way to access a window in deep. +// It returns (window *glfw.Window) which is an unexported member inside a (*pixelgl.Window). +// Read only argument game ignores the pass lock by value warning. +func (g game) WindowDeep() (baseWindow *glfw.Window) { + return *(**glfw.Window)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(g.window)).FieldByName("window").UnsafeAddr())) +} + +// Read only argument game ignores the pass lock by value warning. +func (g game) BridgesCount() (sum int) { + for _, row := range g.ladder.bridges { + for _, col := range row { + if col { + sum++ + } + } + } + return sum +} + +// ------------------------------------------------------------------------- +// Run on main thread + +// Run the game window and its event loop on main thread. +func (g *game) Run() { + pixelgl.Run(func() { + g.RunLazyInit() + g.RunEventLoop() + }) +} + +func (g *game) RunLazyInit() { + // This window will show up as soon as it is created. + win, err := pixelgl.NewWindow(pixelgl.WindowConfig{ + Title: title + " (" + version + ")", + Icon: nil, + Bounds: pixel.R(0, 0, g.winWidth, g.winHeight), + Monitor: nil, + Resizable: true, + // Undecorated: true, + VSync: false, + }) + if err != nil { + panic(err) + } + win.SetSmooth(true) + + MoveWindowToCenterOfPrimaryMonitor := func(win *pixelgl.Window) { + vmodes := pixelgl.PrimaryMonitor().VideoModes() + vmodesLast := vmodes[len(vmodes)-1] + biggestResolution := pixel.R(0, 0, float64(vmodesLast.Width), float64(vmodesLast.Height)) + win.SetPos(biggestResolution.Center().Sub(win.Bounds().Center())) + } + MoveWindowToCenterOfPrimaryMonitor(win) + + // lazy init vars + g.window = win + g.camera = gg.NewCamera(g.ladder.bound.Center(), g.window.Bounds()) + + // register callback + windowGL := g.WindowDeep() + windowGL.SetSizeCallback(func(_ *glfw.Window, width int, height int) { + g.OnResize(float64(width), float64(height)) + }) + + // time manager + g.vsync = time.Tick(time.Second / 120) + g.fpsw.Start() + g.dtw.Start() + + // so-called loading + { + g.window.Clear(colornames.Brown) + screenCenter := g.window.Bounds().Center() + txt := text.New(screenCenter, gg.NewAtlas("", 36, nil)) + txt.WriteString("Loading...") + txt.Draw(g.window, pixel.IM) + g.window.Update() + } + g.NextFrame(g.dtw.Dt()) // Give it a blood pressure. + g.NextFrame(g.dtw.Dt()) // Now the oxygenated blood will start to pump through its vein. + // Do whatever you want after that... + + // from user setting + g.camera.Zoom(float64(g.initialZoomLevel)) + g.camera.Rotate(g.initialRotateDegree) +} + +func (g *game) RunEventLoop() { + for g.window.Closed() != true { // Your average event loop in main thread. + // Notice that all function calls as go routine are non-blocking, but the others will block the main thread. + + // --------------------------------------------------- + // 0. dt + dt := g.dtw.Dt() + + // --------------------------------------------------- + // 1. handling events + g.HandlingEvents(dt) + + // --------------------------------------------------- + // 2. move on + g.NextFrame(dt) + + // log.Println(g.window.Closed()) // + + } // for +} // func + +func (g *game) HandlingEvents(dt float64) { + // Notice that all function calls as go routine are non-blocking, but the others will block the main thread. + + // system + if g.window.JustReleased(pixelgl.KeyEscape) { + g.window.SetClosed(true) + } + if g.window.JustReleased(pixelgl.KeySpace) { + g.Pause() + dialog.Message("%s", "Pause").Title("PPAP").Info() + g.Resume() + } + if g.window.JustReleased(pixelgl.KeyTab) { + if g.window.Monitor() == nil { + g.SetFullScreenMode(true) + } else { + g.SetFullScreenMode(false) + } + } + + // scalpel mode + if g.window.JustReleased(pixelgl.MouseButtonRight) { + go func() { + g.isScalpelMode = !g.isScalpelMode + }() + } + if g.window.JustReleased(pixelgl.MouseButtonLeft) { + // --------------------------------------------------- + if !jukebox.IsPlaying() { + jukebox.Play() + } + + // --------------------------------------------------- + posWin := g.window.MousePosition() + posGame := g.camera.Unproject(posWin) + go func() { + g.explosions.ExplodeAt(pixel.V(posGame.X, posGame.Y), pixel.V(10, 10)) + }() + + // --------------------------------------------------- + if g.isScalpelMode { + // strTitle := fmt.Sprint(posGame.X, ", ", posGame.Y) // + strDlg := fmt.Sprint( + "number of bridges: ", g.BridgesCount(), "\r\n", "\r\n", + "camera angle in degree: ", (g.camera.Angle()/math.Pi)*180, "\r\n", "\r\n", + "camera coordinates: ", g.camera.XY().X, g.camera.XY().Y, "\r\n", "\r\n", + "game clock: ", g.dtw.GetTimeStarted(), "\r\n", "\r\n", + "starfield speed: ", g.galaxy.Speed(), "\r\n", "\r\n", + "mouse click coords in screen pos: ", posWin.X, posWin.Y, "\r\n", "\r\n", + "mouse click coords in game pos: ", posGame.X, posGame.Y, + ) + go func() { + // g.window.SetTitle(strTitle) // + dialog.Message("%s", strDlg).Title("MouseButtonLeft").Info() + }() + } + } + + // game ctrl + if g.window.JustReleased(pixelgl.Key1) { // shuffle + go func() { + g.Shuffle(10, 750) + }() + } + if g.window.JustReleased(pixelgl.Key2) { // find path slow + go func() { + g.ResetPaths() + g.AnimatePaths(g.AnimatePath) + }() + } + if g.window.JustReleased(pixelgl.Key3) { // find path fast + go func() { + g.ResetPaths() + g.AnimatePaths(func(participant int) { + g.AnimatePathInTime(participant, 1) + }) + }() + } + + // camera + if g.window.JustReleased(pixelgl.KeyEnter) { + go func() { + g.camera.Rotate(-90) + }() + } + if g.window.Pressed(pixelgl.KeyRight) { + go func(dt float64) { // This camera will go diagonal while the case is in middle of rotating the camera. + g.camera.Move(pixel.V(1000*dt, 0).Rotated(-g.camera.Angle())) + }(dt) + } + if g.window.Pressed(pixelgl.KeyLeft) { + go func(dt float64) { + g.camera.Move(pixel.V(-1000*dt, 0).Rotated(-g.camera.Angle())) + }(dt) + } + if g.window.Pressed(pixelgl.KeyUp) { + go func(dt float64) { + g.camera.Move(pixel.V(0, 1000*dt).Rotated(-g.camera.Angle())) + }(dt) + } + if g.window.Pressed(pixelgl.KeyDown) { + go func(dt float64) { + g.camera.Move(pixel.V(0, -1000*dt).Rotated(-g.camera.Angle())) + }(dt) + } + { // if scrolled + zoomLevel := g.window.MouseScroll().Y + go func() { + g.camera.Zoom(zoomLevel) + }() + } +} + +func (g *game) NextFrame(dt float64) { + // --------------------------------------------------- + // 1. update - calc state of game objects each frame + g.Update(dt) + g.fpsw.Poll() + + // --------------------------------------------------- + // 2. draw on window + g.window.Clear(g.bg) // clear canvas + g.Draw() // then draw + + // --------------------------------------------------- + // 3. update window - always end with it + g.window.Update() + <-g.vsync +} + +// ------------------------------------------------------------------------- +// On compile + +const title = "AMIDA KUJI" + +var version = "undefined" + +// ------------------------------------------------------------------------- +// Entry point + +func main() { + defer func() { + err := jukebox.Finalize() + if err != nil { + log.Fatal(err) + } + }() + rand.Seed(time.Now().UnixNano()) + + conf := askConf() + if conf == nil { + conf = map[string]interface{}{ + "window_width": 800.0, + "window_height": 800.0, + "max_player": 10.0, + "max_level": 100.0, + "width": 1500.0, + "height": 1500.0, + "zoom": -4.0, + "rotate_degree": 270.0, + "margin_top": 50.0, + "margin_right": 100.0, + "margin_bottom": 50.0, + "margin_left": 200.0, + "font_size": 28.0, + "picks": []interface{}{"Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard", "Squirtle", "Wartortle", "Blastoise", "Caterpie", "Metapod", "Butterfree", "Weedle", "Kakuna", "Beedrill", "Pidgey", "Pidgeotto", "Pidgeot", "Rattata"}, + "prizes": []interface{}{"TM88", "TM89", "TM90", "TM91", "TM92", "HM01", "HM02", "HM03", "HM04", "HM05", "HM06"}, + } + } + + newGame(gameConfig{ + winWidth: conf["window_width"].(float64), + winHeight: conf["window_height"].(float64), + nParticipants: int(conf["max_player"].(float64)), + nLevel: int(conf["max_level"].(float64)), + width: conf["width"].(float64), + height: conf["height"].(float64), + initialZoomLevel: conf["zoom"].(float64), + initialRotateDegree: conf["rotate_degree"].(float64), + paddingTop: conf["margin_top"].(float64), + paddingRight: conf["margin_right"].(float64), + paddingBottom: conf["margin_bottom"].(float64), + paddingLeft: conf["margin_left"].(float64), + fontSize: conf["font_size"].(float64), + nametagPicks: gg.ItfsToStrs(conf["picks"].([]interface{})), + nametagPrizes: gg.ItfsToStrs(conf["prizes"].([]interface{})), + }).Run() +} + +func askConf() (conf map[string]interface{}) { + for { // Load JSON + cwd, _ := os.Getwd() + filepath, err := dialog.File().Title("Load User Settings"). + Filter("JSON Format (*.json)", "json"). + Filter("All Files (*.*)", "*"). + SetStartDir(cwd).Load() + if err != nil { + if err.Error() == "Cancelled" { + conf = nil + break + } + dialog.Message("%s", "Invalid file path."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error() + continue + } + bytes, err := ioutil.ReadFile(filepath) + if err != nil { + dialog.Message("%s", "Could not read the file."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error() + continue + } + err = json.Unmarshal(bytes, &conf) + if err != nil { + dialog.Message("%s", "The file is not valid JSON format."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error() + continue + } + break + } + return +} diff --git a/community/amidakuji/glossary/cam.go b/community/amidakuji/glossary/cam.go new file mode 100644 index 0000000..cfbf386 --- /dev/null +++ b/community/amidakuji/glossary/cam.go @@ -0,0 +1,150 @@ +package glossary + +import ( + "math" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "golang.org/x/image/colornames" +) + +// Camera is a tool to get the screen center to be able to follow a certain point on a plane. +type Camera struct { + anglePhysic float64 // Angle in radians (math.Pi) + angleFollow float64 // Angle expected to be in the near future. + zoomPosPhysic float64 // Z + zoomPosFollow float64 // Z expected to be in the near future. + planePosPhysic pixel.Vec // X, Y + planePosFollow pixel.Vec // X, Y expected to be in the near future. + screenBound pixel.Rect + moveSmooth bool +} + +// NewCamera is a constructor. +func NewCamera(_pos pixel.Vec, _screenBound pixel.Rect) *Camera { + return &Camera{ + anglePhysic: 0, + angleFollow: 0, + zoomPosPhysic: 1.0, + zoomPosFollow: 1.0, + planePosPhysic: _pos, + planePosFollow: _pos, + screenBound: _screenBound, + moveSmooth: true, + } +} + +// ------------------------------------------------------------------------- +// Read only + +// Transform returns a transformation matrix of a camera. +// Use Transform().Project() to convert a game position to a screen position. +// To do the inverse operation, it is recommended to use Camera#Unproject() rather than Transform().Unproject() +func (camera Camera) Transform() pixel.Matrix { + return pixel.IM. // This transformation order is significant. + // ScaledXY(camera.planePos, pixel.V(camera.zoomPos, camera.zoomPos)). + Scaled(camera.planePosPhysic, camera.zoomPosPhysic). // Scaling + Rotated(camera.planePosPhysic, camera.anglePhysic). // Rotatation + Moved(camera.screenBound.Center().Sub(camera.planePosPhysic)) // Translation +} + +// Unproject converts a screen position to a game position. +// This method is a replacement of Transform().Unproject() which might return a bit off position. +func (camera Camera) Unproject(screenPosition pixel.Vec) (gamePosition pixel.Vec) { + matrix1 := pixel.IM. + Scaled(camera.planePosPhysic, camera.zoomPosPhysic). // Scaling + Moved(camera.screenBound.Center().Sub(camera.planePosPhysic)) // Translation + matrix2 := pixel.IM. + Rotated(camera.planePosPhysic, -camera.anglePhysic) // Rotatation + return matrix2.Project(matrix1.Unproject(screenPosition)) +} + +// Angle returns the angle of a camera in radians. +func (camera Camera) Angle() float64 { + return camera.anglePhysic +} + +// XYZ returns a camera's coordinates value X, Y, and Z in a current physical state. +func (camera Camera) XYZ() (float64, float64, float64) { + return camera.planePosPhysic.X, camera.planePosPhysic.Y, camera.zoomPosPhysic +} + +// XY returns the X and Y of a camera as a vector. +func (camera Camera) XY() pixel.Vec { + return camera.planePosPhysic +} + +// Z returns the zoom depth of a camera. +func (camera Camera) Z() float64 { + return camera.zoomPosPhysic +} + +// ------------------------------------------------------------------------- +// Read and Write + +// Update a camera's current physical state (physics) +// by calculating coordinates X, Y, Z and its angle after delta time in seconds. +func (camera *Camera) Update(dt float64) { + if camera.moveSmooth { // lerp the camera position towards the target + angle := pixel.V(camera.anglePhysic, 0) + angleFollow := pixel.V(camera.angleFollow, 0) + angle = pixel.Lerp(angle, angleFollow, 1-math.Pow(1.0/128, dt)) + camera.anglePhysic = angle.X + camera.planePosPhysic = pixel.Lerp(camera.planePosPhysic, camera.planePosFollow, 1-math.Pow(1.0/128, dt)) + zoomPos := pixel.V(camera.zoomPosPhysic, 0) + zoomFollow := pixel.V(camera.zoomPosFollow, 0) + zoomPos = pixel.Lerp(zoomPos, zoomFollow, 1-math.Pow(1.0/128, dt)) + camera.zoomPosPhysic = zoomPos.X + } else { + camera.anglePhysic = camera.angleFollow + camera.planePosPhysic = camera.planePosFollow + camera.zoomPosPhysic = camera.zoomPosFollow + } +} + +// Rotate a camera by certain degrees. +// + ) Counterclockwise +// - ) Clockwise +func (camera *Camera) Rotate(degree float64) { + camera.angleFollow += degree * math.Pi / 180 +} + +// Zoom in and out with a camera by certain levels. +// + ) Zoom in +// - ) Zoom out +func (camera *Camera) Zoom(byLevel float64) { + const zoomAmount = 1.2 + camera.zoomPosFollow *= math.Pow(zoomAmount, byLevel) +} + +// Move camera a specified distance. +func (camera *Camera) Move(distance pixel.Vec) { + camera.planePosFollow = camera.planePosFollow.Add(distance) +} + +// MoveTo () moves a camera to a point on a plane. +func (camera *Camera) MoveTo(posAim pixel.Vec) { + camera.planePosFollow = posAim +} + +// SetScreenBound of a camera. +func (camera *Camera) SetScreenBound(screenBound pixel.Rect) { + camera.screenBound = screenBound +} + +// ------------------------------------------------------------------- +// Unnecessary + +// Aim for experiments. +type Aim struct { + pos pixel.Vec +} + +// Draw aim as a dot. +func (aim Aim) Draw(t pixel.Target) { + imd := imdraw.New(nil) + imd.Color = colornames.Red + imd.Push(aim.pos) + imd.Circle(10, 0) + imd.Draw(t) +} diff --git a/community/amidakuji/glossary/dtchk.go b/community/amidakuji/glossary/dtchk.go new file mode 100644 index 0000000..cf22d2c --- /dev/null +++ b/community/amidakuji/glossary/dtchk.go @@ -0,0 +1,67 @@ +package glossary + +import ( + "errors" + "time" +) + +// DtWatch is a delta time checker. +type DtWatch struct { + init *time.Time + last *time.Time +} + +// Start () is required in order to call other methods of a DtWatch. +func (watch *DtWatch) Start() { + byVal1 := time.Now() + byVal2 := byVal1 + watch.init = &byVal1 + watch.last = &byVal2 +} + +// IsStarted () determines whether it has started or not. +// .Start() is required in order to call this method. +func (watch DtWatch) IsStarted() bool { + if watch.init == nil { + return false + } else if watch.init != nil { + return true + } else { + panic(errors.New("It might be thread")) + } +} + +// GetTimeStarted gets the time it started. +// .Start() must be called prior to calling this method. +func (watch DtWatch) GetTimeStarted() time.Time { + return *watch.init +} + +// SetTimeStarted sets the time it started. +// .Start() must be called prior to calling this method. +func (watch *DtWatch) SetTimeStarted(t time.Time) { + *watch.init = t +} + +// Dt since last Dt() or DtNano(). +// .Start() must be called prior to calling this method. +func (watch *DtWatch) Dt() (deltaTimeInSeconds float64) { + deltaTimeInSeconds = time.Since(time.Time(*watch.last)).Seconds() + *watch.last = time.Now() + return +} + +// DtNano since last Dt() or DtNano(). +// It returns a time instance with nanosecond precision. +// .Start() must be called prior to calling this method. +func (watch *DtWatch) DtNano() (deltaTimeInNanosec time.Duration) { + deltaTimeInNanosec = time.Since(time.Time(*watch.last)) + *watch.last = time.Now() + return +} + +// DtSinceStart is dt since last Start(). +// .Start() must be called prior to calling this method. +func (watch DtWatch) DtSinceStart() (deltaTimeInSeconds float64) { + return time.Since(time.Time(*watch.init)).Seconds() +} diff --git a/community/amidakuji/glossary/explosive.go b/community/amidakuji/glossary/explosive.go new file mode 100644 index 0000000..f5255d6 --- /dev/null +++ b/community/amidakuji/glossary/explosive.go @@ -0,0 +1,198 @@ +package glossary + +import ( + "image/color" + "math/rand" + "sync" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" +) + +// ------------------------------------------------------------------------- +// explosive.go +// - Original idea: "github.com/faiface/pixel/examples/community/bouncing" + +// -------------------------------------------------------------------- + +// Explosions is an imdraw and a manager of all particles. +type Explosions struct { + imd *imdraw.IMDraw + mutex sync.Mutex // It is unsafe to access any refd; ptrd object without a critical section. + // + *colorPicker + width float64 + height float64 + particles []*particle + precision int +} + +// NewExplosions is a constructor. +// The 3rd argument colors can be nil. Then it will use its default value of a color set. +func NewExplosions(width, height float64, colors []color.Color, precision int) *Explosions { + return &Explosions{ + nil, sync.Mutex{}, + newColorPicker(colors), + width, height, nil, + precision, + } +} + +// SetBound of particles. All particles bounce when they meet this bound. +func (e *Explosions) SetBound(width, height float64) { + e.width = width + e.height = height +} + +// IsExploding determines whether this Explosions is about to be updated or not. +// Pass lock by value warning from (e Explosions) should be ignored, +// because an Explosions here is just passed as a read only argument. +func (e Explosions) IsExploding() bool { + e.mutex.Lock() + defer e.mutex.Unlock() + + return e.particles != nil +} + +// Draw guarantees the thread safety, though it's not a necessary condition. +// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods. +func (e *Explosions) Draw(t pixel.Target) { + e.mutex.Lock() + defer e.mutex.Unlock() + + if e.imd == nil || len(e.particles) <= 0 { // isInvisible set to true. + return // An empty image is drawn. + } + + e.imd.Draw(t) +} + +// Update animates an Explosions. An Explosions is drawn on an imdraw. +func (e *Explosions) Update(dt float64) { + e.mutex.Lock() + defer e.mutex.Unlock() + + // physics + aliveParticles := []*particle{} + for _, particle := range e.particles { + particle.update(dt, e.width, e.height) + if particle.life > 0 { + aliveParticles = append(aliveParticles, particle) + } + } + e.particles = aliveParticles + + // imdraw (a state machine) + if e.imd == nil { // lazy creation + e.imd = imdraw.New(nil) + e.imd.EndShape = imdraw.RoundEndShape + e.imd.Precision = e.precision + } + imd := e.imd + imd.Clear() + + // draw + for _, particle := range e.particles { + imd.Color = particle.color + imd.Push(particle.pos) + imd.Circle(16*particle.life, 0) + } +} + +// ExplodeAt generates an explosion at given point. +func (e *Explosions) ExplodeAt(pos, vel pixel.Vec) { + e.mutex.Lock() + defer e.mutex.Unlock() + + e.next() + e.particles = append(e.particles, + newParticleAt(pos, vel.Rotated(1).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(2).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(3).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(4).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(5).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(6).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(7).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(8).Scaled(rand.Float64()), e.here()), + newParticleAt(pos, vel.Rotated(9).Scaled(rand.Float64()), e.here()), + + newParticleAt(pos, vel.Rotated(10).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(20).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(30).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(40).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(50).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(60).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(70).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(80).Scaled(rand.Float64()+1), e.here()), + newParticleAt(pos, vel.Rotated(90).Scaled(rand.Float64()+1), e.here()), + ) +} + +// -------------------------------------------------------------------- + +type particle struct { + pos pixel.Vec + vel pixel.Vec + color color.RGBA + life float64 +} + +func newParticleAt(pos, vel pixel.Vec, color color.RGBA) *particle { + color.A = 5 + return &particle{pos, vel, color, rand.Float64() * 1.5} +} + +func (p *particle) update(dt, width, height float64) { + p.pos = p.pos.Add(p.vel) + p.life -= 3 * dt + switch { + case p.pos.Y < 0 || p.pos.Y >= height: + p.vel.Y *= (-10 * dt) + case p.pos.X < 0 || p.pos.X >= width: + p.vel.X *= (-10 * dt) + } +} + +// -------------------------------------------------------------------- + +type colorPicker struct { + colors []color.RGBA + index int +} + +func newColorPicker(_colors []color.Color) *colorPicker { + if _colors == nil { + _colors = []color.Color{ + color.RGBA{190, 38, 51, 255}, + color.RGBA{224, 111, 139, 255}, + color.RGBA{73, 60, 43, 255}, + color.RGBA{164, 100, 34, 255}, + color.RGBA{235, 137, 49, 255}, + color.RGBA{247, 226, 107, 255}, + color.RGBA{47, 72, 78, 255}, + color.RGBA{68, 137, 26, 255}, + color.RGBA{163, 206, 39, 255}, + color.RGBA{0, 87, 132, 255}, + color.RGBA{49, 162, 242, 255}, + color.RGBA{178, 220, 239, 255}, + } + } + colors := []color.RGBA{} + for _, v := range _colors { + if c, ok := v.(color.RGBA); ok { + colors = append(colors, c) + } + } + return &colorPicker{colors, 0} +} + +func (colorPicker *colorPicker) next() color.RGBA { + if colorPicker.index++; colorPicker.index >= len(colorPicker.colors) { + colorPicker.index = 0 + } + return colorPicker.colors[colorPicker.index] +} + +func (colorPicker *colorPicker) here() color.RGBA { + return colorPicker.colors[colorPicker.index] +} diff --git a/community/amidakuji/glossary/fpschk.go b/community/amidakuji/glossary/fpschk.go new file mode 100644 index 0000000..513a16c --- /dev/null +++ b/community/amidakuji/glossary/fpschk.go @@ -0,0 +1,134 @@ +package glossary + +import ( + "fmt" + "image/color" + "sync" + "time" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "github.com/faiface/pixel/text" + "golang.org/x/image/colornames" +) + +// FPSWatch measures the real-time frame rates and displays it on a target canvas. +type FPSWatch struct { + txt *text.Text // shared variable + atlas *text.Atlas // borrowed atlas for txt + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize + // + fps int // The FPS evaluated every second. + frames int // Frames count before the FPS update. + seccer <-chan time.Time // Ticks time every second. + // + desc string + pos pixel.Vec + anchorX AnchorX + anchorY AnchorY + colorBg color.Color + colorTxt color.Color +} + +// NewFPSWatch is a constructor. +func NewFPSWatch( + additionalCaption string, _pos pixel.Vec, + _anchorY AnchorY, _anchorX AnchorX, // This is because the order is usually Y then X in spoken language. + _colorBg, _colorTxt color.Color, +) (watch *FPSWatch) { + return &FPSWatch{ + atlas: AtlasASCII(), + fps: 0, + frames: 0, + seccer: nil, + desc: additionalCaption, + pos: _pos, + anchorX: _anchorX, + anchorY: _anchorY, + colorBg: _colorBg, + colorTxt: _colorTxt, + } +} + +// NewFPSWatchSimple is a constructor. +func NewFPSWatchSimple(_pos pixel.Vec, _anchorY AnchorY, _anchorX AnchorX) *FPSWatch { + return NewFPSWatch("", _pos, _anchorY, _anchorX, colornames.Black, colornames.White) +} + +// Start ticking every second. +func (watch *FPSWatch) Start() { + watch.seccer = time.Tick(time.Second) +} + +// Poll () should be called only once and in every single frame. (Obligatory) +// This is an extended behavior of Update() like funcs. +func (watch *FPSWatch) Poll() { + watch.frames++ + select { + case <-watch.seccer: + watch.fps = watch.frames + watch.frames = 0 + go watch._Update() + default: + } +} + +// SetPos to a position in screen coords. +func (watch *FPSWatch) SetPos(pos pixel.Vec, anchorY AnchorY, anchorX AnchorX) { + watch.pos = pos + watch.anchorX = anchorX + watch.anchorY = anchorY +} + +// GetFPS returns the most recent FPS recorded. +// A non-ptr FPSWatch as a read only argument passes lock by value within itself but that seems totally fine. +func (watch FPSWatch) GetFPS() int { + return watch.fps +} + +// Draw FPSWatch. +func (watch *FPSWatch) Draw(t pixel.Target) { + // lock before accessing txt & imdraw + watch.mutex.Lock() + defer watch.mutex.Unlock() + + if watch.imd == nil && watch.txt == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + watch.imd.Draw(t) + watch.txt.Draw(t, pixel.IM) +} + +// unexported +func (watch *FPSWatch) _Update() { + // lock before txt & imdraw update + watch.mutex.Lock() + defer watch.mutex.Unlock() + + // text label (a state machine) + if watch.txt == nil { // lazy creation + watch.txt = text.New(pixel.ZV, watch.atlas) + } + txt := watch.txt + txt.Clear() + + str := fmt.Sprint("FPS: ", watch.fps, " ", watch.desc) + AnchorTxt(txt, watch.pos, watch.anchorX, watch.anchorY, str) + txt.Color = watch.colorTxt + txt.Dot.X -= 1.0 + txt.Dot.Y += 5.0 + txt.WriteString(str) + + // imdraw (a state machine) + if watch.imd == nil { // lazy creation + watch.imd = imdraw.New(nil) + } + imd := watch.imd + imd.Clear() + + imd.Color = watch.colorBg + imd.Push(VerticesOfRect(txt.Bounds())...) + imd.Polygon(0) +} diff --git a/community/amidakuji/glossary/jukebox/music.go b/community/amidakuji/glossary/jukebox/music.go new file mode 100644 index 0000000..22e54ea --- /dev/null +++ b/community/amidakuji/glossary/jukebox/music.go @@ -0,0 +1,130 @@ +package jukebox + +import ( + "errors" + "io/ioutil" + "os" + "sync" + "time" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + + "github.com/faiface/beep" + "github.com/faiface/beep/speaker" + "github.com/faiface/beep/vorbis" +) + +// ------------------------------------------------------------------------- + +const nMusics = 2 + +var ( + mutex sync.Mutex + isPlaying bool + musics [nMusics]*_Music +) + +// ------------------------------------------------------------------------- + +// singleton +func init() { + // city pop favorites + musics[0] = _NewMusicFromAsset("nighttempo-purepresent1", "karaoke/kikuchimomoko-nightcruising.ogg") + musics[1] = _NewMusicFromAsset("nighttempo-purepresent2", "karaoke/takeuchimariya-plasticlove.ogg") + + // speaker on + speaker.Init(musics[0].format.SampleRate, musics[0].format.SampleRate.N(time.Second)) + speaker.Play(beep.Iterate(func() (soundtrack beep.Streamer) { + musics[0].stream.Seek(0) + musics[1].stream.Seek(0) + return beep.Seq(musics[0].stream, musics[1].stream) + })) + speaker.Lock() +} + +// IsPlaying determines whether the soundtrack is currently playing or not. +func IsPlaying() bool { + return isPlaying +} + +// Play unlocks the speaker. +func Play() { + mutex.Lock() + defer mutex.Unlock() + if !isPlaying { + isPlaying = true + speaker.Unlock() + } + return +} + +// Pause locks the speaker. +func Pause() { + mutex.Lock() + defer mutex.Unlock() + if isPlaying { + isPlaying = false + speaker.Lock() + } + return +} + +// Finalize should be called on program exit. +// This function deletes the temporary music file its package generates. +func Finalize() error { + errs := "" + for _, music := range musics { + music.Close() + err := music._Destory() + if err != nil { + errs += " " + err.Error() + } + } + if errs != "" { + return errors.New(errs) + } + return nil +} + +// ------------------------------------------------------------------------- + +// NewMusicFromAsset is a constructor. +func _NewMusicFromAsset(nameMusic, nameAsset string) *_Music { + asset, err := gg.Asset(nameAsset) + if err != nil { + // log.Fatal(err) // + } + return _NewMusic(nameMusic, asset) +} + +// Music is a temporary file to play a single background music. It should be destroyed on program exit. +type _Music struct { + os.File + stream beep.StreamSeekCloser + format beep.Format +} + +// NewMusic creates an instance of Music, a temporary file from which the speaker plays a music. +// speaker.Lock() to pause. +// speaker.Unlock() to resume/play. +func _NewMusic(name string, asset []byte) *_Music { + tmpfile, err := ioutil.TempFile("", name) + if err != nil { + // log.Fatal(err) // + } + // log.Println(tmpfile.Name()) // + _, err = tmpfile.Write(asset) + if err != nil { + // log.Fatal(err) // + } + stream, format, err := vorbis.Decode(tmpfile) + if err != nil { + // log.Fatal(err) // + } + return &_Music{*tmpfile, stream, format} +} + +// Destory deletes the temporary music file. +func (music *_Music) _Destory() error { + return os.Remove(music.Name()) +} diff --git a/community/amidakuji/glossary/starfield.go b/community/amidakuji/glossary/starfield.go new file mode 100644 index 0000000..9b0090f --- /dev/null +++ b/community/amidakuji/glossary/starfield.go @@ -0,0 +1,166 @@ +package glossary + +import ( + "image/color" + "math/rand" + "sync" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" +) + +// ------------------------------------------------------------------------- +// Reusable modified starfiled +// - Original: "github.com/faiface/pixel/examples/community/starfield" +// - Encapsulated by nanitefactory + +// ------------------------------------------------------------------------- +// Galaxy + +type star struct { + Pos pixel.Vec // x, y + Z float64 // z + P float64 // prev z + C color.RGBA // color +} + +// Galaxy is an imd of stars. +type Galaxy struct { + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize + // + width float64 + height float64 + speed float64 + stars [1024]*star +} + +// NewGalaxy is a constructor. +func NewGalaxy(_width, _height, _speed float64) *Galaxy { + return &Galaxy{ + width: _width, + height: _height, + speed: _speed, + } +} + +// Speed is a getter. +// Pass lock by value warning from (galaxy Galaxy) should be ignored, +// because a galaxy here is just passed as a read only argument. +func (galaxy Galaxy) Speed() float64 { + return galaxy.speed +} + +// SetSpeed is a setter. +func (galaxy *Galaxy) SetSpeed(_speed float64) { + galaxy.speed = _speed +} + +// Draw guarantees the thread safety, though it's not a necessary condition. +// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods. +func (galaxy *Galaxy) Draw(t pixel.Target) { + galaxy.mutex.Lock() + defer galaxy.mutex.Unlock() + + if galaxy.imd == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + galaxy.imd.Draw(t) +} + +// Update animates a galaxy. +func (galaxy *Galaxy) Update(dt float64) { + // random() + random := func(min, max float64) float64 { + return rand.Float64()*(max-min) + min + } + + // newStar() + newStar := func() *star { + starColors := []color.RGBA{ + color.RGBA{157, 180, 255, 255}, + color.RGBA{162, 185, 255, 255}, + color.RGBA{167, 188, 255, 255}, + color.RGBA{170, 191, 255, 255}, + color.RGBA{175, 195, 255, 255}, + color.RGBA{186, 204, 255, 255}, + color.RGBA{192, 209, 255, 255}, + color.RGBA{202, 216, 255, 255}, + color.RGBA{228, 232, 255, 255}, + color.RGBA{237, 238, 255, 255}, + color.RGBA{251, 248, 255, 255}, + color.RGBA{255, 249, 249, 255}, + color.RGBA{255, 245, 236, 255}, + color.RGBA{255, 244, 232, 255}, + color.RGBA{255, 241, 223, 255}, + color.RGBA{255, 235, 209, 255}, + color.RGBA{255, 215, 174, 255}, + color.RGBA{255, 198, 144, 255}, + color.RGBA{255, 190, 127, 255}, + color.RGBA{255, 187, 123, 255}, + color.RGBA{255, 187, 123, 255}, + } // Colors based on stellar types listed at // http://www.vendian.org/mncharity/dir3/starcolor/ + return &star{ + Pos: pixel.V(random(-galaxy.width, galaxy.width), random(-galaxy.height, galaxy.height)), + Z: random(0, galaxy.width), + P: 0, + C: starColors[rand.Intn(len(starColors))], + } + } + + // lock before imdraw update + galaxy.mutex.Lock() + defer galaxy.mutex.Unlock() + + // imdraw (a state machine) + if galaxy.imd == nil { // lazy creation + galaxy.imd = imdraw.New(nil) + galaxy.imd.SetMatrix(pixel.IM.Moved(pixel.V(galaxy.width/2, galaxy.height/2))) + } + imd := galaxy.imd + imd.Clear() + imd.Precision = 7 + + // now update all stars in this galaxy + for i, s := range galaxy.stars { + if s == nil { + galaxy.stars[i] = newStar() + s = galaxy.stars[i] + } + + scale := func(unscaledNum, min, max, minAllowed, maxAllowed float64) float64 { + return (maxAllowed-minAllowed)*(unscaledNum-min)/(max-min) + minAllowed + } + + s.P = s.Z + s.Z -= dt * galaxy.speed + + if s.Z < 0 { + s.Pos.X = random(-galaxy.width, galaxy.width) + s.Pos.Y = random(-galaxy.height, galaxy.height) + s.Z = galaxy.width + s.P = s.Z + } + + p := pixel.V( + scale(s.Pos.X/s.Z, 0, 1, 0, galaxy.width), + scale(s.Pos.Y/s.Z, 0, 1, 0, galaxy.height), + ) + + o := pixel.V( + scale(s.Pos.X/s.P, 0, 1, 0, galaxy.width), + scale(s.Pos.Y/s.P, 0, 1, 0, galaxy.height), + ) + + r := scale(s.Z, 0, galaxy.width, 11, 0) + + galaxy.imd.Color = s.C + if p.Sub(o).Len() > 6 { + galaxy.imd.Push(p, o) + galaxy.imd.Line(r) + } + galaxy.imd.Push(p) + galaxy.imd.Circle(r, 0) + } +} diff --git a/community/amidakuji/glossary/util.go b/community/amidakuji/glossary/util.go new file mode 100644 index 0000000..a3e7b04 --- /dev/null +++ b/community/amidakuji/glossary/util.go @@ -0,0 +1,215 @@ +package glossary + +import ( + "bytes" + "fmt" + "image" + "io/ioutil" + "math" + "math/rand" + "os" + + // Relevant packages of target format for a decoder must be initialized to register. + _ "image/gif" + _ "image/png" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/text" + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" +) + +var atlasASCII *text.Atlas + +func init() { + atlasASCII = NewAtlas("", 18, nil) +} + +// AtlasASCII returns an atlas which allows you to draw only ASCII characters. +// Atlas is a set of generated textures for glyphs in a specific font. +func AtlasASCII() *text.Atlas { + return atlasASCII +} + +// NewAtlas newly loads and prepares a set of images of characters or symbols to be drawn. +// Arg runeSet would be set to nil if non-ASCII characters are not in use. +func NewAtlas(nameAssetTTF string, size float64, runeSet []rune) *text.Atlas { + if nameAssetTTF == "" { + nameAssetTTF = "NanumBarunGothic.ttf" + } + + var face font.Face + asset, err := Asset(nameAssetTTF) + if err == nil { + face, err = LoadTrueTypeFont(asset, size) + } + if err != nil { + face = basicfont.Face7x13 + } + return text.NewAtlas(face, text.ASCII, runeSet) +} + +// NewSprite converts an asset (resource) into a sprite. Returns nil if there is an error. +// AssetNames() or AssetDir() might be helpful when utilizing this function. +func NewSprite(nameAsset string) *pixel.Sprite { + asset, err := Asset(nameAsset) + if err != nil { + // log.Println("1", err) // + return nil + } + pic, err := LoadPicture(asset) + if err != nil { + // log.Println("2", err) // + return nil + } + // log.Println("3", "success yay") // + return pixel.NewSprite(pic, pic.Bounds()) +} + +// LoadTrueTypeFontFromFile creates and returns a font face. +func LoadTrueTypeFontFromFile(path string, size float64) (font.Face, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + bytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + face, err := LoadTrueTypeFont(bytes, size) + if err != nil { + return nil, err + } + + return face, nil +} + +// LoadPictureFromFile decodes an image that has been encoded in a registered format. (png, jpg, etc.) +// Format registration is typically done by an init function in the codec-specific package. (with underscore import) +func LoadPictureFromFile(path string) (pixel.Picture, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + return pixel.PictureDataFromImage(img), nil +} + +// LoadTrueTypeFont creates and returns a font face. +func LoadTrueTypeFont(bytes []byte, size float64) (font.Face, error) { + font, err := truetype.Parse(bytes) + if err != nil { + return nil, err + } + return truetype.NewFace(font, &truetype.Options{ + Size: size, + GlyphCacheEntries: 1, + }), nil +} + +// LoadPicture decodes an image that has been encoded in a registered format. (png, jpg, etc.) +// Format registration is typically done by an init function in the codec-specific package. (with underscore import) +func LoadPicture(_bytes []byte) (pixel.Picture, error) { + img, _, err := image.Decode(bytes.NewReader(_bytes)) + if err != nil { + return nil, err + } + return pixel.PictureDataFromImage(img), nil +} + +// RandomNiceColor from Platformer. +// Is not completely random without rand.Seed(). +func RandomNiceColor() pixel.RGBA { +again: + r := rand.Float64() + g := rand.Float64() + b := rand.Float64() + len := math.Sqrt(r*r + g*g + b*b) + if len == 0 { + goto again + } + return pixel.RGB(r/len, g/len, b/len) +} + +// VerticesOfRect returns 4 vertices of a rectangle in a form of a slice of vectors. +func VerticesOfRect(r pixel.Rect) []pixel.Vec { + return []pixel.Vec{ + r.Min, + pixel.V(r.Max.X, r.Min.Y), + r.Max, + pixel.V(r.Min.X, r.Max.Y), + } +} + +// ItfsToStrs converts []interface{} to []string. +func ItfsToStrs(itfs []interface{}) (strs []string) { + strs = make([]string, len(itfs)) + for i, v := range itfs { + strs[i] = fmt.Sprint(v) + } + return strs +} + +// Direction returns a direction as a normalized vector. This vector always has a length of 1. +func Direction(from, to pixel.Vec) (dirVecNormalized pixel.Vec) { + vec := to.Sub(from) + if vec.X == 0 && vec.Y == 0 { + return vec + } + return vec.Unit() +} + +// ------------------------------------------------------------------------- +// Anchors + +// AnchorY - Top, Middle, Bottom +type AnchorY int + +// enum AnchorY +const ( + Top AnchorY = 1 + iota + Middle + Bottom +) + +// AnchorX - Left, Center, Right +type AnchorX int + +// enum AnchorX +const ( + Left AnchorX = 1 + iota + Center + Right +) + +// AnchorTxt positions a text.Text label with an anchor alignment. +func AnchorTxt(txt *text.Text, pos pixel.Vec, anchorX AnchorX, anchorY AnchorY, desc string) { + txt.Orig = pos + txt.Dot = pos + switch anchorX { + case Left: + txt.Dot.X -= 0 + case Center: + txt.Dot.X -= (txt.BoundsOf(desc).W() / 2) + case Right: + txt.Dot.X -= txt.BoundsOf(desc).W() + } + switch anchorY { + case Top: + txt.Dot.Y -= txt.BoundsOf(desc).H() + case Middle: + txt.Dot.Y -= (txt.BoundsOf(desc).H() / 2) + case Bottom: + txt.Dot.Y -= 0 + } + txt.Dot.X += 0 + txt.Dot.Y += 0 +} diff --git a/community/amidakuji/ladder.go b/community/amidakuji/ladder.go new file mode 100644 index 0000000..14fd257 --- /dev/null +++ b/community/amidakuji/ladder.go @@ -0,0 +1,334 @@ +package main + +import ( + "math/rand" + "sync" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "golang.org/x/image/colornames" +) + +// Ladder is an imdraw that does not animate at all, +// hence does not need to be modified every frame. +// Something kinda static and bone-like. +type Ladder struct { + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize + // + bound pixel.Rect + grid [][]pixel.Vec + bridges [][]bool + nParticipants int + nLevel int + paddingTop float64 + paddingRight float64 + paddingBottom float64 + paddingLeft float64 + colors []pixel.RGBA +} + +// NewLadder is a constructor. +func NewLadder(_nParticipants, _nLevel int, + _width, _height, + _paddingTop, _paddingRight, + _paddingBottom, _paddingLeft float64) *Ladder { + + // get random colors + colors := []pixel.RGBA{} + for i := 0; i < _nParticipants; i++ { + colors = append(colors, gg.RandomNiceColor()) + } + + // new grid + newGrid := func(nRow, nCol int) [][]pixel.Vec { + arr := make([][]pixel.Vec, nRow) + for i := range arr { + arr[i] = make([]pixel.Vec, nCol) + } + // log.Println(arr) // + return arr + } + + // new bridges + newBridges := func(nParticipants, nLevel int) [][]bool { + nRow := nParticipants - 1 + nCol := nLevel + arr := make([][]bool, nRow) + for i := range arr { + arr[i] = make([]bool, nCol) + } + return arr + } + + // init ladder + l := Ladder{ + imd: imdraw.New(nil), + bound: pixel.R(0, 0, _width, _height), + grid: newGrid(_nParticipants, _nLevel), + bridges: newBridges(_nParticipants, _nLevel), + nParticipants: _nParticipants, + nLevel: _nLevel, + paddingTop: _paddingTop, + paddingBottom: _paddingBottom, + paddingRight: _paddingRight, + paddingLeft: _paddingLeft, + colors: colors, + } + + // init grid + updateGrid := func(l *Ladder) { + for participant := range l.grid { // row + for level := range l.grid[participant] { // col + y := l.Height() - (float64(participant) * l.DistParticipant()) // reverse + x := float64(level) * l.DistLevel() + y -= l.paddingTop + x += l.paddingLeft + l.grid[participant][level] = pixel.V(x, y) + } + } + } // Indices would not be aligned with the screen coordinates. (Reverse Y - Rows) + updateGrid(&l) + + // log.Println(l.grid) // + return &l +} + +// ------------------------------------------------------------------------- +// Important methods + +// Draw guarantees the thread safety, though it's not a necessary condition. +// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods. +func (l *Ladder) Draw(t pixel.Target) { + l.mutex.Lock() + defer l.mutex.Unlock() + + if l.imd == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + l.imd.Draw(t) +} + +// Update draws a ladder on an imdraw. +func (l *Ladder) Update() { + ptsMovedAbout := func(sub pixel.Vec, pts ...pixel.Vec) (ptsMoved []pixel.Vec) { + ptsMoved = make([]pixel.Vec, len(pts)) + copy(ptsMoved, pts) + for i, vec := range ptsMoved { + ptsMoved[i] = vec.Sub(sub) + } + // log.Println(ptsMoved) // debug + // log.Println(pts) // debug + return ptsMoved + } + + ptsStart := l.PtsAtLevelOfPicks() + ptsEnd := l.PtsAtLevelOfPrizes() + + circleRadius := 20.0 + circlePts := ptsMovedAbout(pixel.V(circleRadius+10, 0), ptsStart...) + + // lock shared imdraw access + l.mutex.Lock() + defer l.mutex.Unlock() + + // imdraw (a state machine) + if l.imd == nil { // lazy creation + l.imd = imdraw.New(nil) + } + imd := l.imd + imd.Clear() + + // draw lanes + imd.Color = colornames.White + imd.EndShape = imdraw.RoundEndShape + for i := range ptsStart { + imd.Push(ptsStart[i], ptsEnd[i]) + imd.Line(13) + } + + // draw bridges + imd.Color = colornames.White + imd.EndShape = imdraw.RoundEndShape + for nrow, row := range l.bridges { + for ncol, e := range row { + if e { + imd.Push(l.grid[nrow][ncol], l.grid[nrow+1][ncol]) + imd.Line(13) + } + } + } + + // draw start points + imd.EndShape = imdraw.RoundEndShape + for i := range ptsStart { + imd.Color = l.colors[i] + imd.Push(circlePts[i]) + } + imd.Circle(circleRadius, 0) +} + +// ------------------------------------------------------------------------- +// Read only methods + +// Height returns the height of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) Height() float64 { + return l.bound.H() +} + +// Width returns the width of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) Width() float64 { + return l.bound.W() +} + +// DistLevel returns the distance between each level. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) DistLevel() float64 { + return (l.Width() - (l.paddingLeft + l.paddingRight)) / float64(l.nLevel-1) +} + +// DistParticipant returns the distance between each lane. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) DistParticipant() float64 { + return (l.Height() - (l.paddingTop + l.paddingBottom)) / float64(l.nParticipants-1) +} + +// PtsAtLevelOfPicks returns all starting points of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) PtsAtLevelOfPicks() (ret []pixel.Vec) { + const levelOfDraw int = 0 // where it starts + ret = make([]pixel.Vec, l.nParticipants, l.nParticipants) + + for participant := range l.grid { // row + ret[participant] = l.grid[participant][levelOfDraw] + } + + // log.Println(len(ret), ret) // + return ret //[:l.nParticipants] // +} + +// PtAtLevelOfPicks returns a starting point of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) PtAtLevelOfPicks(participant int) pixel.Vec { + const levelOfDraw int = 0 // where it starts + return l.grid[participant][levelOfDraw] +} + +// PtsAtLevelOfPrizes returns all end points of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) PtsAtLevelOfPrizes() (ret []pixel.Vec) { + levelOfPrize := l.nLevel - 1 // where it ends + ret = make([]pixel.Vec, l.nParticipants, l.nParticipants) + + for participant := range l.grid { // row + ret[participant] = l.grid[participant][levelOfPrize] + } + + // log.Println(len(ret), ret) // + return ret //[:l.nParticipants] // +} + +// PtAtLevelOfPrizes returns an end point of a ladder. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (l Ladder) PtAtLevelOfPrizes(participant int) pixel.Vec { + levelOfPrize := l.nLevel - 1 // where it ends + return l.grid[participant][levelOfPrize] +} + +// ------------------------------------------------------------------- +// Methods that write to itself + +// ClearBridges of a ladder. +// Only values are changed, not the pointers. +func (l *Ladder) ClearBridges() { + for _, row := range l.bridges { + for i := range row { + row[i] = false + } + } +} + +// GenerateRandomBridges of an approximate amount. +// Only values are changed, not the pointers. +func (l *Ladder) GenerateRandomBridges(amountApprox int) { + pickOneBridgeInRandom := func(l *Ladder) { + nRow := len(l.bridges) + nCol := l.nLevel + row := rand.Intn(int(nRow)) // participant + col := rand.Intn(int(nCol)) // level + // check right + isOkRight := func(rowRight, col int) bool { + includeLowerBound := func(rowRight int) bool { + return rowRight >= 0 + } + if !includeLowerBound(rowRight) { // out of bound + return true + } + if !l.bridges[rowRight][col] { + return true + } + return false + } + // check left + isOkLeft := func(rowLeft, col int) bool { + excludeUpperBound := func(rowLeft int) bool { + return rowLeft < len(l.bridges) + } + if !excludeUpperBound(rowLeft) { // out of bound + return true + } + if !l.bridges[rowLeft][col] { + return true + } + return false + } + rowRight := row - 1 + rowLeft := row + 1 + if isOkRight(rowRight, col) && isOkLeft(rowLeft, col) { + l.bridges[row][col] = true + } + } // func + + // repeat + for i := 0; i < amountApprox; i++ { + pickOneBridgeInRandom(l) + } +} // method + +// RegenerateRandomBridges clears out all bridges and then GenerateRandomBridges() an approximate amount. +// Only values are changed, not the pointers. +func (l *Ladder) RegenerateRandomBridges(amountApprox int) { + l.ClearBridges() + l.GenerateRandomBridges(amountApprox) +} + +// RegenerateRandomColors sets all colors of a ladder random for each. +// Only values are changed, not the pointers. +func (l *Ladder) RegenerateRandomColors() { + for i := range l.colors { + l.colors[i] = gg.RandomNiceColor() + } +} + +// Reset all bridges and colors. +// Only values are changed, not the pointers. +func (l *Ladder) Reset() { + aboutTwo := l.nParticipants * (l.nLevel - 1) * 2 + aboutOne := l.nParticipants * (l.nLevel - 1) + aboutHalf := (l.nParticipants * (l.nLevel - 1)) / 2 + // + var pick [4]int + pick[0] = aboutTwo + pick[1] = aboutOne + pick[2] = aboutHalf + i := rand.Intn(3) + // log.Println(i) // + // + l.RegenerateRandomBridges(pick[i]) + l.RegenerateRandomColors() +} diff --git a/community/amidakuji/nametag.go b/community/amidakuji/nametag.go new file mode 100644 index 0000000..3acf5c1 --- /dev/null +++ b/community/amidakuji/nametag.go @@ -0,0 +1,126 @@ +package main + +import ( + "image/color" + "sync" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "github.com/faiface/pixel/text" + "golang.org/x/image/colornames" +) + +// Nametags is a list(slice) of nametags. +type Nametags []Nametag + +// Update nametags. +func (updaters Nametags) Update() { + for i := range updaters { + updaters[i].Update() + } +} + +// Draw nametags. +func (updaters Nametags) Draw(t pixel.Target) { + for i := range updaters { + updaters[i].Draw(t) + } +} + +// Nametag for each nametag. +type Nametag struct { + txt *text.Text // shared variable + atlas *text.Atlas // borrowed atlas for txt + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize + // + desc string + pos pixel.Vec + anchorX gg.AnchorX + anchorY gg.AnchorY + colorBg color.Color + colorTxt color.Color +} + +// NewNametag is a constructor. +func NewNametag( + _atlas *text.Atlas, + _desc string, _pos pixel.Vec, + _anchorY gg.AnchorY, // This is because the order is usually Y then X in spoken language. + _anchorX gg.AnchorX, + _colorBg, _colorTxt color.Color) *Nametag { + atlas := _atlas + if atlas == nil { + atlas = gg.AtlasASCII() + } + return &Nametag{ + atlas: atlas, + desc: _desc, + pos: _pos, + anchorX: _anchorX, + anchorY: _anchorY, + colorBg: _colorBg, + colorTxt: _colorTxt, + } +} + +// NewNametagSimple is a constructor. +func NewNametagSimple( + _atlas *text.Atlas, + _desc string, _pos pixel.Vec, + _anchorY gg.AnchorY, + _anchorX gg.AnchorX, +) *Nametag { + return NewNametag(_atlas, _desc, _pos, _anchorY, _anchorX, colornames.Wheat, colornames.Black) +} + +// String of a nametag. +// A getter and a callback which allows a nametag to be passed to a function as a string. +// A non-ptr Nametag as a read only argument passes lock by value within itself but that seems totally fine. +func (n Nametag) String() string { + return n.desc +} + +// Draw a nametag. +func (n *Nametag) Draw(t pixel.Target) { + n.mutex.Lock() + defer n.mutex.Unlock() + + if n.imd == nil && n.txt == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + n.imd.Draw(t) + n.txt.Draw(t, pixel.IM) +} + +// Update a nametag. +func (n *Nametag) Update() { + // lock before txt & imdraw update + n.mutex.Lock() + defer n.mutex.Unlock() + + // text label (a state machine) + if n.txt == nil { // lazy creation + n.txt = text.New(pixel.ZV, n.atlas) + } + txt := n.txt + txt.Clear() + + gg.AnchorTxt(txt, n.pos, n.anchorX, n.anchorY, n.desc) + txt.Color = n.colorTxt + txt.WriteString(n.desc) + + // imdraw (a state machine) + if n.imd == nil { // lazy creation + n.imd = imdraw.New(nil) + } + imd := n.imd + imd.Clear() + + imd.Color = n.colorBg + imd.Push(gg.VerticesOfRect(txt.Bounds())...) + imd.Polygon(0) +} diff --git a/community/amidakuji/path.go b/community/amidakuji/path.go new file mode 100644 index 0000000..4761d65 --- /dev/null +++ b/community/amidakuji/path.go @@ -0,0 +1,281 @@ +package main + +import ( + "math" + "sync" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" +) + +// Path is for animating a path to the prize in a ladder. +type Path struct { + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize + // + roads []pixel.Vec // A list of vectors - each vector for a position where a road starts. + prize *int + tip *pixel.Vec + tipDir pixel.Vec + iroad int + // + watchAnim gg.DtWatch // When it started to animate. + timeLimitAnimInSec float64 + animateInTime bool + isAnimating bool + + // ----------------------------------------------------------- + // exported callbacks(listeners) regarding animation + + // callback on reaching the prize level of a ladder. + OnFinishedAnimation func() + + // callback when the animating 'tip' passes a point of a road. + // pt: a point(road) just passed. + // dir: ... + // dir is a normalized vector. (pixel.ZV) is passed if the direction can't be found. + // dir can be different depending on how fast this Path is updated. + OnPassedEachPoint func(pt pixel.Vec, dir pixel.Vec) +} + +// NewPath is a contructor. +func NewPath(_roads []pixel.Vec, _prize *int) *Path { + newTip := func() *pixel.Vec { + if _roads != nil { + if len(_roads) > 0 { + v := _roads[0] + return &v + } + } + return nil + } + return &Path{ + roads: _roads, + prize: _prize, + tip: newTip(), + tipDir: pixel.ZV, + } +} + +// NewPathEmpty is a contructor. +func NewPathEmpty() *Path { + return &Path{} +} + +// ------------------------------------------------------------------------- +// Important methods + +// Draw guarantees the thread safety, though it's not a necessary condition. +// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods. +func (path *Path) Draw(t pixel.Target) { + path.mutex.Lock() + defer path.mutex.Unlock() + + if path.imd == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + path.imd.Draw(t) +} + +// Update animates a path. A path is drawn on an imdraw. +func (path *Path) Update(color pixel.RGBA) { + var ( + iroad = len(path.roads) - 1 + from = path.roads[len(path.roads)-1] + to = path.roads[len(path.roads)-1] + dir = pixel.ZV + ) + + if path.isAnimating { + // get where it is abstract // get a scalar + dt := path.watchAnim.DtSinceStart() + const distPerSec = 500 + scalarProgress := dt * distPerSec + // log.Println(dt, path.Len(), scalarProgress) // + + if path.animateInTime { // overwrite scalarProgress + fromLot := pixel.V(0, 0) + toPrize := pixel.V(path.Len(), 0) + percentagePointPerSec := 1 / path.timeLimitAnimInSec + scalarProgress = pixel.Lerp(fromLot, toPrize, dt*percentagePointPerSec).X + // log.Println(dt, path.Len(), scalarProgress, dt*percentagePointPerSec) // + } + + // get where it is concrete // turn a scalar into a set of vectors + iroad, from, to, dir = path.FindRoadByDist(scalarProgress) + if iroad > path.iroad { + if path.OnPassedEachPoint != nil { + go path.OnPassedEachPoint(from, dir) + } + } + path.iroad = iroad + // log.Println(iroad, len(path.roads), iroad == len(path.roads)-1, path.isAnimating) // + if iroad >= len(path.roads)-1 { // the end + path.isAnimating = false + // log.Println(path.Len(), dt) // + if path.OnFinishedAnimation != nil { + go path.OnFinishedAnimation() + } // callback + } + } + + // lock before imdraw update + path.mutex.Lock() + defer path.mutex.Unlock() + + // imdraw (a state machine) + if path.imd == nil { // lazy creation + path.imd = imdraw.New(nil) + } + imd := path.imd + imd.Clear() + + // draw path + imd.Color = color + imd.EndShape = imdraw.RoundEndShape + for i := 0; i < iroad; i++ { + imd.Push(path.roads[i], path.roads[i+1]) + imd.Line(9) + } + imd.Push(from, to) + imd.Line(9) + + // save where the tip is + path.tip = &to +} + +// ------------------------------------------------------------------------- +// Read only methods + +// IsAnimating determines whether this Path is about to be updated or not. +// Pass lock by value warning from (path Path) should be ignored, +// because a Path here is just passed as a read only argument. +func (path Path) IsAnimating() bool { + return path.isAnimating +} + +// GetPrize is just an average getter. +// It returns -1 if the receiver is not initialized with that member(prize). +// Pass lock by value warning from (path Path) should be ignored, +// because a Path here is just passed as a read only argument. +func (path Path) GetPrize() int { + if path.prize == nil { + return -1 + } + return *path.prize +} + +// PosTip returns a vector that tells you how far the animating path currently has reached. +// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine. +func (path Path) PosTip() (v pixel.Vec) { + return *path.tip +} + +// Len returns the total length of all roads. +// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine. +func (path Path) Len() (sum float64) { + for i := 0; i < len(path.roads)-1; i++ { + sum += math.Abs(path.roads[i].Sub(path.roads[i+1]).Len()) + } + return +} + +// FindRoadByDist converts a scalar into a set of vectors. +// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine. +// +// Returns +// iroad: The index of a road found. +// road: The vector representation of a road found. A road is a line from pt A to B, and that vector points to where pt A is. +// pos: A position(point) found which is in the middle of that found road(line). +// dirVecNormalized: A direction as a normalized vector. This vector always has a length of 1. +func (path Path) FindRoadByDist(distProgress float64) (iroad int, road pixel.Vec, pos pixel.Vec, dirVecNormalized pixel.Vec) { + lengthOfTraveledRoads := float64(0.0) + for iroad = 0; iroad < len(path.roads)-1; iroad++ { + var lengthOfThisRoad float64 + iroadNext := iroad + 1 + lengthOfThisRoad = math.Abs(path.roads[iroad].Sub(path.roads[iroadNext]).Len()) + lengthOfTraveledRoads += lengthOfThisRoad + // For loop breaker: distProgress is somewhere between the total length of a path. + if lengthOfTraveledRoads > distProgress { + scalar := lengthOfThisRoad - (lengthOfTraveledRoads - distProgress) + if path.roads[iroad].Y == path.roads[iroadNext].Y && + path.roads[iroad].X < path.roads[iroadNext].X { // to the bottom (east) + pos = path.roads[iroad] + pos.X += scalar + dirVecNormalized = pixel.V(1, 0) + } else if path.roads[iroad].X == path.roads[iroadNext].X && + path.roads[iroad].Y > path.roads[iroadNext].Y { // to the left (south) + pos = path.roads[iroad] + pos.Y -= scalar + dirVecNormalized = pixel.V(0, -1) + } else if path.roads[iroad].X == path.roads[iroadNext].X && + path.roads[iroad].Y < path.roads[iroadNext].Y { // to the right (north) + pos = path.roads[iroad] + pos.Y += scalar + dirVecNormalized = pixel.V(0, 1) + } else if path.roads[iroad].Y == path.roads[iroadNext].Y && + path.roads[iroad].X > path.roads[iroadNext].X { // to the top (west) + // Placed at the end of an elif statement, + // since this case is of no possibility unless the path finding is going reverse. + pos = path.roads[iroad] + pos.X -= scalar + dirVecNormalized = pixel.V(-1, 0) + } else { + panic("unhandled exception: it may be a diagonal bridge") + } // elif + return iroad, path.roads[iroad], pos, dirVecNormalized + } // if - for loop breaker + } // for + + // coming down to here means that the case is (road == pos) + from := iroad - 1 + to := iroad + if iroad == 0 { + from = iroad + to = iroad + 1 + } + if from < 0 || to >= len(path.roads) { + dirVecNormalized = pixel.ZV + } else { + dirVecNormalized = gg.Direction(path.roads[from], path.roads[to]) + } + return iroad, path.roads[iroad], path.roads[iroad], dirVecNormalized +} + +// ------------------------------------------------------------------------- +// Methods that write to itself + +// Animate a path. +func (path *Path) Animate() { + path.watchAnim.Start() + path.animateInTime = false + path.isAnimating = true +} + +// AnimateInTime animates a path in given time. +func (path *Path) AnimateInTime(sec float64) { + path.watchAnim.Start() + path.timeLimitAnimInSec = sec + path.animateInTime = true + path.isAnimating = true +} + +// Pause a path's clock. +func (path *Path) Pause() { + if path.watchAnim.IsStarted() { + path.watchAnim.Dt() + } +} + +// Resume after pause. +func (path *Path) Resume() { + if path.watchAnim.IsStarted() { + started := path.watchAnim.GetTimeStarted() + dtPause := path.watchAnim.DtNano() + path.watchAnim.SetTimeStarted(started.Add(dtPause)) + // log.Println(dtPause, started, path.watchAnim.GetTimeStarted()) // + } +} diff --git a/community/amidakuji/scalpel.go b/community/amidakuji/scalpel.go new file mode 100644 index 0000000..6976cc8 --- /dev/null +++ b/community/amidakuji/scalpel.go @@ -0,0 +1,118 @@ +package main + +import ( + "image/color" + "sync" + + gg "github.com/faiface/pixel/examples/community/amidakuji/glossary" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "golang.org/x/image/colornames" +) + +// Scalpel is a surgical knife for dissection and surgery. And for debugging purposes sometimes. +type Scalpel struct { + imd *imdraw.IMDraw // shared variable + mutex sync.Mutex // synchronize +} + +// Draw guarantees the thread safety, though it's not a necessary condition. +// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods. +func (s *Scalpel) Draw(t pixel.Target) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.imd == nil { // isInvisible set to true. + return // An empty image is drawn. + } + + s.imd.Draw(t) +} + +// Update dissects a ladder. The anatomy of a ladder is drawn on an imdraw. +// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine. +func (s *Scalpel) Update(l Ladder) { + ptsEnd := l.PtsAtLevelOfPrizes() + + // lock shared imdraw access + s.mutex.Lock() + defer s.mutex.Unlock() + + // imdraw (a state machine) + if s.imd == nil { // lazy creation + s.imd = imdraw.New(nil) + } + imd := s.imd + imd.Clear() + + // draw bounds + imd.Color = colornames.Black + imd.EndShape = imdraw.NoEndShape + imd.Push(gg.VerticesOfRect(l.bound)...) + imd.Polygon(4) + + // draw end points + imd.Color = colornames.Blueviolet + imd.Push(ptsEnd...) + imd.Circle(10, 0) + + // draw grid + imd.Color = colornames.Red + for nrow, row := range l.grid { + for ncol := range row { + imd.Push(l.grid[nrow][ncol]) + } + } + imd.Circle(5, 0) +} + +// ------------------------------------------------------------------------- + +// UpdateDrawProjekt has nothing to do with scalpel. +func UpdateDrawProjekt(t pixel.Target, rekt pixel.Rect, color color.Color, matrix pixel.Matrix) { + imd := imdraw.New(nil) + + imd.Color = color + imd.EndShape = imdraw.NoEndShape + vertices := gg.VerticesOfRect(rekt) + for i, v := range vertices { + vertices[i] = matrix.Project(v) + } + imd.Push(vertices...) + imd.Polygon(10) + + imd.Draw(t) +} + +// UpdateDrawUnprojekt has nothing to do with scalpel. +func UpdateDrawUnprojekt(t pixel.Target, rekt pixel.Rect, color color.Color, matrix pixel.Matrix) { + imd := imdraw.New(nil) + + imd.Color = color + imd.EndShape = imdraw.NoEndShape + vertices := gg.VerticesOfRect(rekt) + for i, v := range vertices { + vertices[i] = matrix.Unproject(v) + } + imd.Push(vertices...) + imd.Polygon(10) + + imd.Draw(t) +} + +// UpdateDrawUnprojekt2 has nothing to do with scalpel. +func UpdateDrawUnprojekt2(t pixel.Target, rekt pixel.Rect, color color.Color, camera gg.Camera) { + imd := imdraw.New(nil) + + imd.Color = color + imd.EndShape = imdraw.NoEndShape + vertices := gg.VerticesOfRect(rekt) + for i, v := range vertices { + vertices[i] = camera.Unproject(v) + } + imd.Push(vertices...) + imd.Polygon(10) + + imd.Draw(t) +}