├── .gitignore ├── hello └── main.go ├── js └── main.go ├── channels └── main.go ├── ebitengine ├── index.html ├── LICENSE ├── crt.go └── main.go ├── export ├── main.go └── index.html ├── main.go ├── vugu ├── main_wasm.go ├── index.html ├── root.vugu └── root_vgen.go ├── test └── fetch_test.go ├── LICENSE ├── fetch └── main.go ├── vecty ├── index.html └── main.go ├── go.mod ├── canvas ├── index.html ├── main.go └── LICENSE ├── Makefile ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | html/* 3 | -------------------------------------------------------------------------------- /hello/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func main() { 8 | fmt.Println("Hello World") 9 | } 10 | -------------------------------------------------------------------------------- /js/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import "syscall/js" 6 | 7 | var document js.Value 8 | 9 | func init() { 10 | document = js.Global().Get("document") 11 | } 12 | 13 | func main() { 14 | div := document.Call("getElementById", "target") 15 | 16 | node := document.Call("createElement", "div") 17 | node.Set("innerText", "Hello World") 18 | 19 | div.Call("appendChild", node) 20 | } 21 | -------------------------------------------------------------------------------- /channels/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | func sum(s []int, c chan int) { 8 | sum := 0 9 | for _, v := range s { 10 | sum += v 11 | } 12 | c <- sum // send sum to c 13 | } 14 | 15 | func main() { 16 | s := []int{7, 2, 8, 9, -4, 0} 17 | 18 | c := make(chan int) 19 | go sum(s[:len(s)/2], c) 20 | go sum(s[len(s)/2:], c) 21 | x, y := <-c, <-c // receive from c 22 | 23 | fmt.Println(x, y, x+y) 24 | } 25 | -------------------------------------------------------------------------------- /ebitengine/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /ebitengine/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Ebiten Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | -------------------------------------------------------------------------------- /export/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "syscall/js" 7 | ) 8 | 9 | var document js.Value 10 | 11 | func init() { 12 | document = js.Global().Get("document") 13 | } 14 | 15 | func main() { 16 | document.Set("appendText", js.FuncOf(appendText)) 17 | 18 | // Prevent main from exiting 19 | select {} 20 | } 21 | 22 | func appendText(_ js.Value, _ []js.Value) interface{} { 23 | msg := document.Call("getElementById", "input").Get("value").String() 24 | 25 | p := document.Call("createElement", "p") 26 | p.Set("innerHTML", msg) 27 | document.Call("getElementById", "target").Call("appendChild", p) 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /ebitengine/crt.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | //kage:unit pixels 4 | 5 | // Reference: a public domain CRT effect 6 | // https://github.com/libretro/glsl-shaders/blob/master/crt/shaders/crt-lottes.glsl 7 | 8 | package main 9 | 10 | func Warp(pos vec2) vec2 { 11 | const ( 12 | warpX = 0.031 13 | warpY = 0.041 14 | ) 15 | pos = pos*2 - 1 16 | pos *= vec2(1+(pos.y*pos.y)*warpX, 1+(pos.x*pos.x)*warpY) 17 | return pos/2 + 0.5 18 | } 19 | 20 | func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { 21 | // Adjust the texture position to [0, 1]. 22 | origin := imageSrc0Origin() 23 | size := imageSrc0Size() 24 | pos := srcPos 25 | pos -= origin 26 | pos /= size 27 | 28 | pos = Warp(pos) 29 | return imageSrc0At(pos*size + origin) 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/NYTimes/gziphandler" 10 | ) 11 | 12 | var gz = flag.Bool("gzip", false, "enable automatic gzip compression") 13 | 14 | func main() { 15 | flag.Parse() 16 | h := wasmContentTypeSetter(http.FileServer(http.Dir("./html"))) 17 | if *gz { 18 | h = gziphandler.GzipHandler(h) 19 | } 20 | 21 | log.Print("Serving on http://localhost:8080") 22 | err := http.ListenAndServe(":8080", h) 23 | if err != http.ErrServerClosed { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | func wasmContentTypeSetter(h http.Handler) http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | if strings.HasSuffix(r.URL.Path, ".wasm") { 31 | w.Header().Set("content-type", "application/wasm") 32 | } 33 | h.ServeHTTP(w, r) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /vugu/main_wasm.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | 8 | "flag" 9 | 10 | "github.com/vugu/vugu" 11 | "github.com/vugu/vugu/domrender" 12 | ) 13 | 14 | func main() { 15 | 16 | mountPoint := flag.String("mount-point", "#vugu_mount_point", "The query selector for the mount point for the root component, if it is not a full HTML component") 17 | flag.Parse() 18 | 19 | fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint) 20 | defer fmt.Printf("Exiting main()\n") 21 | 22 | buildEnv, err := vugu.NewBuildEnv() 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | renderer, err := domrender.New(*mountPoint) 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer renderer.Release() 32 | 33 | rootBuilder := &Root{} 34 | 35 | for ok := true; ok; ok = renderer.EventWait() { 36 | 37 | buildResults := buildEnv.RunBuild(rootBuilder) 38 | 39 | err = renderer.Render(buildResults) 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /test/fetch_test.go: -------------------------------------------------------------------------------- 1 | package fetch_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | type jsonResp struct { 10 | Headers struct { 11 | Accept string `json:"Accept"` 12 | AcceptEncoding string `json:"Accept-Encoding"` 13 | Host string `json:"Host"` 14 | Origin string `json:"Origin"` 15 | Referer string `json:"Referer"` 16 | UserAgent string `json:"User-Agent"` 17 | } `json:"headers"` 18 | Origin string `json:"origin"` 19 | URL string `json:"url"` 20 | } 21 | 22 | func TestFetch(t *testing.T) { 23 | u := "https://httpbin.org/get" 24 | resp, err := http.Get(u) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | defer resp.Body.Close() 29 | 30 | if resp.StatusCode != http.StatusOK { 31 | t.Fatalf("Unexpected StatusCode %d", resp.StatusCode) 32 | } 33 | 34 | var r jsonResp 35 | err = json.NewDecoder(resp.Body).Decode(&r) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if r.URL != u { 41 | t.Errorf("Unexpected request URL: %q", r.URL) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Johan Brandhorst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /fetch/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "syscall/js" 11 | ) 12 | 13 | var document js.Value 14 | 15 | func init() { 16 | document = js.Global().Get("document") 17 | } 18 | 19 | type writer js.Value 20 | 21 | // Write implements io.Writer. 22 | func (d writer) Write(p []byte) (n int, err error) { 23 | node := document.Call("createElement", "div") 24 | node.Set("textContent", string(p)) 25 | js.Value(d).Call("appendChild", node) 26 | return len(p), nil 27 | } 28 | 29 | func main() { 30 | t := document.Call("getElementById", "target") 31 | logger := log.New((*writer)(&t), "", log.LstdFlags) 32 | 33 | c := http.Client{} 34 | req, err := http.NewRequest( 35 | "POST", 36 | "https://httpbin.org/anything", 37 | strings.NewReader(`{"test":"test"}`), 38 | ) 39 | if err != nil { 40 | logger.Fatal(err) 41 | } 42 | 43 | resp, err := c.Do(req) 44 | if err != nil { 45 | logger.Fatal(err) 46 | } 47 | defer resp.Body.Close() 48 | b, err := ioutil.ReadAll(resp.Body) 49 | if err != nil { 50 | logger.Fatal(err) 51 | } 52 | logger.Print(string(b)) 53 | } 54 | -------------------------------------------------------------------------------- /vugu/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 12 |
13 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /vecty/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/johanbrandhorst/wasm-experiments 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.0 6 | 7 | require ( 8 | github.com/NYTimes/gziphandler v1.1.1 9 | github.com/gopherjs/vecty v0.5.0 10 | github.com/hajimehoshi/ebiten/v2 v2.7.2 11 | github.com/microcosm-cc/bluemonday v1.0.26 12 | github.com/slimsag/blackfriday v2.0.0+incompatible 13 | github.com/vugu/vjson v0.0.0-20200505061711-f9cbed27d3d9 14 | github.com/vugu/vugu v0.3.5 15 | ) 16 | 17 | require ( 18 | github.com/aymerick/douceur v0.2.0 // indirect 19 | github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 // indirect 20 | github.com/ebitengine/hideconsole v1.0.0 // indirect 21 | github.com/ebitengine/oto/v3 v3.2.0 // indirect 22 | github.com/ebitengine/purego v0.7.0 // indirect 23 | github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984 // indirect 24 | github.com/gorilla/css v1.0.1 // indirect 25 | github.com/jezek/xgb v1.1.1 // indirect 26 | github.com/jfreymuth/oggvorbis v1.0.5 // indirect 27 | github.com/jfreymuth/vorbis v1.0.2 // indirect 28 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 29 | github.com/vugu/xxhash v0.0.0-20191111030615-ed24d0179019 // indirect 30 | golang.org/x/image v0.15.0 // indirect 31 | golang.org/x/net v0.24.0 // indirect 32 | golang.org/x/sync v0.6.0 // indirect 33 | golang.org/x/sys v0.19.0 // indirect 34 | golang.org/x/text v0.14.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /export/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | Go wasm 12 | 13 | 14 | 15 | 20 | 21 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /vugu/root.vugu: -------------------------------------------------------------------------------- 1 |
2 |
Loading...
3 |
4 |
Updated:
5 | 10 |
11 | 12 |
13 | 14 | 70 | -------------------------------------------------------------------------------- /vecty/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/gopherjs/vecty" 7 | "github.com/gopherjs/vecty/elem" 8 | "github.com/gopherjs/vecty/event" 9 | "github.com/microcosm-cc/bluemonday" 10 | "github.com/slimsag/blackfriday" 11 | ) 12 | 13 | func main() { 14 | vecty.SetTitle("Markdown Demo") 15 | vecty.RenderBody(&PageView{ 16 | Input: `# Markdown Example 17 | 18 | This is a live editor, try editing the Markdown on the right of the page. 19 | `, 20 | }) 21 | } 22 | 23 | // PageView is our main page component. 24 | type PageView struct { 25 | vecty.Core 26 | Input string 27 | } 28 | 29 | // Render implements the vecty.Component interface. 30 | func (p *PageView) Render() vecty.ComponentOrHTML { 31 | return elem.Body( 32 | // Display a textarea on the right-hand side of the page. 33 | elem.Div( 34 | vecty.Markup( 35 | vecty.Style("float", "right"), 36 | ), 37 | elem.TextArea( 38 | vecty.Markup( 39 | vecty.Style("font-family", "monospace"), 40 | vecty.Property("rows", 14), 41 | vecty.Property("cols", 70), 42 | 43 | // When input is typed into the textarea, update the local 44 | // component state and rerender. 45 | event.Input(func(e *vecty.Event) { 46 | p.Input = e.Target.Get("value").String() 47 | vecty.Rerender(p) 48 | }), 49 | ), 50 | vecty.Text(p.Input), // initial textarea text. 51 | ), 52 | ), 53 | 54 | // Render the markdown. 55 | &Markdown{Input: p.Input}, 56 | ) 57 | } 58 | 59 | // Markdown is a simple component which renders the Input markdown as sanitized 60 | // HTML into a div. 61 | type Markdown struct { 62 | vecty.Core 63 | Input string `vecty:"prop"` 64 | } 65 | 66 | // Render implements the vecty.Component interface. 67 | func (m *Markdown) Render() vecty.ComponentOrHTML { 68 | // Render the markdown input into HTML using Blackfriday. 69 | unsafeHTML := blackfriday.Run([]byte(m.Input)) 70 | 71 | // Sanitize the HTML. 72 | safeHTML := string(bluemonday.UGCPolicy().SanitizeBytes(unsafeHTML)) 73 | 74 | // Return the HTML, which we guarantee to be safe / sanitized. 75 | return elem.Div( 76 | vecty.Markup( 77 | vecty.UnsafeHTML(safeHTML), 78 | ), 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /canvas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | go webassembly - repulsion 5 | 6 | 7 | 8 | 9 | 35 | 81 | 82 | 83 | 84 |
85 |
86 |
87 | 88 |
89 |
0
90 |
91 | 92 | 160 93 |
94 |
95 | 100 97 |
98 |
99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: hello 2 | hello: clean 3 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./hello/main.go 4 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 5 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 6 | 7 | .PHONY: tinygo 8 | tinygo: clean 9 | docker run --rm -v $$(pwd):/go/src/github.com/johanbrandhorst/wasm-experiments tinygo/tinygo:latest /bin/bash -c "\ 10 | cd /go/src/github.com/johanbrandhorst/wasm-experiments && \ 11 | tinygo build -o ./html/test.wasm -target wasm --no-debug ./$(target)/main.go && \ 12 | cp /usr/local/tinygo/targets/wasm_exec.js ./html/wasm_exec.js\ 13 | " 14 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 15 | sed -i -e 's;;\n\t
;' ./html/index.html 16 | 17 | .PHONY: channels 18 | channels: clean 19 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./channels/main.go 20 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 21 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 22 | 23 | .PHONY: js 24 | js: clean 25 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./js/main.go 26 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 27 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 28 | sed -i -e 's;;\n\t
;' ./html/index.html 29 | 30 | .PHONY: fetch 31 | fetch: clean 32 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./fetch/main.go 33 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 34 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 35 | sed -i -e 's;;\n\t
;' ./html/index.html 36 | 37 | .PHONY: canvas 38 | canvas: clean 39 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./canvas/main.go 40 | cp ./canvas/index.html ./html/index.html 41 | cp ./canvas/main.go ./html/main.go 42 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 43 | 44 | tinygo-canvas: clean 45 | docker run --rm -v $$(pwd):/go/src/github.com/johanbrandhorst/wasm-experiments tinygo/tinygo:latest /bin/bash -c "\ 46 | cd /go/src/github.com/johanbrandhorst/wasm-experiments && \ 47 | tinygo build -o ./html/test.wasm -target wasm --no-debug ./canvas/main.go && \ 48 | cp /usr/local/tinygo/targets/wasm_exec.js ./html/wasm_exec.js\ 49 | " 50 | cp ./canvas/index.html ./html/index.html 51 | cp ./canvas/main.go ./html/main.go 52 | 53 | .PHONY: ebitengine 54 | ebitengine: clean 55 | GOOS=js GOARCH=wasm go build -o ./html/ebitengine.wasm ./ebitengine/main.go 56 | cp ./ebitengine/index.html ./html/index.html 57 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 58 | 59 | .PHONY: vugu 60 | vugu: clean 61 | go install github.com/vugu/vugu/cmd/vugugen@latest 62 | vugugen --skip-go-mod ./vugu/ 63 | GOOS=js GOARCH=wasm go build -o ./html/main.wasm ./vugu/ 64 | cp ./vugu/index.html ./html/ 65 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 66 | 67 | .PHONY: vecty 68 | vecty: clean 69 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./vecty/main.go 70 | cp ./vecty/index.html ./html/index.html 71 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 72 | 73 | .PHONY: export 74 | export: clean 75 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./export/main.go 76 | cp ./export/index.html ./html/index.html 77 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 78 | 79 | .PHONY: test 80 | test: install-test 81 | GOOS=js GOARCH=wasm go test -v ./test/ 82 | 83 | clean: 84 | rm -f ./html/* 85 | 86 | install-test: 87 | go install github.com/agnivade/wasmbrowsertest@latest 88 | mv $$(go env GOPATH)/bin/wasmbrowsertest $$(go env GOPATH)/bin/go_js_wasm_exec 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-experiments 2 | 3 | Just some playing around with the experimental Go WebAssembly arch target. 4 | 5 | ## Requirements 6 | 7 | Requires `go` >= 1.21.0. 8 | [TinyGo](https://github.com/tinygo-org/tinygo) examples require `docker`. 9 | 10 | ## Basic instructions 11 | 12 | Choose your target from the experiments. Compile with `make ` and serve 13 | with `go run main.go`. This starts a local web server which serves the `./html` 14 | directory. It ensures `.wasm` files are served with their appropriate 15 | content-type. 16 | 17 | For example: 18 | 19 | ```bash 20 | $ make hello 21 | rm -f ./html/* 22 | GOOS=js GOARCH=wasm go build -o ./html/test.wasm ./hello/main.go 23 | cp $(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 24 | cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js 25 | $ go run main.go 26 | 2019/02/24 14:11:51 Serving on http://localhost:8080 27 | ``` 28 | 29 | Navigate to http://localhost:8080 to load the page. Some examples require opening 30 | the browser console to be seen. 31 | 32 | ## Gzip compression 33 | 34 | Use the `-gzip` flag to enable on-the-fly gzip compression of the files: 35 | 36 | ```bash 37 | $ go run main.go -gzip 38 | 2019/02/24 14:11:51 Serving on http://localhost:8080 39 | ``` 40 | 41 | ## TinyGo targets 42 | 43 | To compile something with the TinyGo WebAssembly compiler, simply choose the 44 | target and invoke the `tinygo` make rule with the target specified and 45 | serve as usual. For example: 46 | 47 | ```bash 48 | $ make tinygo target=hello 49 | docker run --rm -v $$(pwd):/go/src/github.com/johanbrandhorst/wasm-experiments tinygo/tinygo:latest /bin/bash -c "\ 50 | cd /go/src/github.com/johanbrandhorst/wasm-experiments && \ 51 | tinygo build -o ./html/test.wasm -target wasm --no-debug ./hello/main.go && \ 52 | cp /usr/local/tinygo/targets/wasm_exec.js ./html/wasm_exec.js\ 53 | " 54 | cp $$(go env GOROOT)/misc/wasm/wasm_exec.html ./html/index.html 55 | sed -i -e 's;;\n\t
;' ./html/index.html 56 | $ go run main.go 57 | 2019/02/24 14:33:58 Serving on http://localhost:8080 58 | ``` 59 | 60 | Note that some of the targets either do not compile or panic at runtime when 61 | compiled with TinyGo. The following targets have been tested to work with 62 | TinyGo: 63 | 64 | - `hello` 65 | - `channels` 66 | - `js` 67 | - `tinygo-canvas` 68 | 69 | ## Experiments 70 | 71 | ### Hello 72 | 73 | A simple `Hello World` example that prints to the browser console. 74 | 75 | ### Channels 76 | 77 | Showing basic first class support for channel operations in compiled WebAssembly. 78 | 79 | ### JS 80 | 81 | A simple example of how to interact with the JavaScript world from WebAssembly. 82 | 83 | ### Fetch 84 | 85 | A more complicated example showing how to use `net/http` `DefaultClient` to 86 | send a HTTP request, parse the result and write it to the DOM. 87 | 88 | ### Canvas 89 | 90 | An updated version of the [repulsion](https://stdiopt.github.io/gowasm-experiments/repulsion) 91 | demo by [Luis Figuerido](https://github.com/stdiopt). 92 | 93 | ### Ebitengine 94 | 95 | A short demo of using the [Ebitengine game engine](https://github.com/hajimehoshi/ebiten) 96 | to create a WebGL based flappy bird clone. Copied from the Ebitengine examples. 97 | 98 | ### Vugu 99 | 100 | An example of a [Vugu](https://www.vugu.org/) application reading the latest 101 | bitcoin price. 102 | 103 | ### Vecty 104 | 105 | An example of a [Vecty](https://github.com/gopherjs/vecty) application automatically 106 | translating the input textbox to rendered markdown. 107 | 108 | ### Export 109 | 110 | An example of exporting a Go function to JavaScript. 111 | -------------------------------------------------------------------------------- /canvas/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | "math/rand" 9 | "strconv" 10 | "syscall/js" 11 | ) 12 | 13 | var ( 14 | width float64 15 | height float64 16 | mousePos [2]float64 17 | ctx js.Value 18 | lineDistSq float64 = 100 * 100 19 | ) 20 | 21 | func main() { 22 | 23 | // Init Canvas stuff 24 | doc := js.Global().Get("document") 25 | canvasEl := doc.Call("getElementById", "mycanvas") 26 | width = doc.Get("body").Get("clientWidth").Float() 27 | height = doc.Get("body").Get("clientHeight").Float() 28 | canvasEl.Call("setAttribute", "width", width) 29 | canvasEl.Call("setAttribute", "height", height) 30 | ctx = canvasEl.Call("getContext", "2d") 31 | 32 | done := make(chan struct{}, 0) 33 | 34 | dt := DotThing{speed: 160} 35 | 36 | mouseMoveEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 37 | e := args[0] 38 | mousePos[0] = e.Get("clientX").Float() 39 | mousePos[1] = e.Get("clientY").Float() 40 | return nil 41 | }) 42 | defer mouseMoveEvt.Release() 43 | 44 | countChangeEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 45 | evt := args[0] 46 | intVal, err := strconv.Atoi(evt.Get("target").Get("value").String()) 47 | if err != nil { 48 | println("Invalid value", err) 49 | return nil 50 | } 51 | dt.SetNDots(intVal) 52 | return nil 53 | }) 54 | defer countChangeEvt.Release() 55 | 56 | speedInputEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 57 | evt := args[0] 58 | fval, err := strconv.ParseFloat(evt.Get("target").Get("value").String(), 64) 59 | if err != nil { 60 | println("Invalid value", err) 61 | return nil 62 | } 63 | dt.speed = fval 64 | return nil 65 | }) 66 | defer speedInputEvt.Release() 67 | 68 | // Handle mouse 69 | doc.Call("addEventListener", "mousemove", mouseMoveEvt) 70 | doc.Call("getElementById", "count").Call("addEventListener", "change", countChangeEvt) 71 | doc.Call("getElementById", "speed").Call("addEventListener", "input", speedInputEvt) 72 | 73 | dt.SetNDots(100) 74 | dt.lines = false 75 | var renderFrame js.Func 76 | var tmark float64 77 | var markCount = 0 78 | var tdiffSum float64 79 | 80 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 81 | now := args[0].Float() 82 | tdiff := now - tmark 83 | tdiffSum += now - tmark 84 | markCount++ 85 | if markCount > 10 { 86 | doc.Call("getElementById", "fps").Set("innerHTML", fmt.Sprintf("FPS: %.01f", 1000/(tdiffSum/float64(markCount)))) 87 | tdiffSum, markCount = 0, 0 88 | } 89 | tmark = now 90 | 91 | // Pool window size to handle resize 92 | curBodyW := doc.Get("body").Get("clientWidth").Float() 93 | curBodyH := doc.Get("body").Get("clientHeight").Float() 94 | if curBodyW != width || curBodyH != height { 95 | width, height = curBodyW, curBodyH 96 | canvasEl.Set("width", width) 97 | canvasEl.Set("height", height) 98 | } 99 | dt.Update(tdiff / 1000) 100 | 101 | js.Global().Call("requestAnimationFrame", renderFrame) 102 | return nil 103 | }) 104 | defer renderFrame.Release() 105 | 106 | // Start running 107 | js.Global().Call("requestAnimationFrame", renderFrame) 108 | 109 | <-done 110 | } 111 | 112 | // DotThing manager 113 | type DotThing struct { 114 | dots []*Dot 115 | lines bool 116 | speed float64 117 | } 118 | 119 | // Update updates the dot positions and draws 120 | func (dt *DotThing) Update(dtTime float64) { 121 | if dt.dots == nil { 122 | return 123 | } 124 | ctx.Call("clearRect", 0, 0, width, height) 125 | 126 | // Update 127 | for i, dot := range dt.dots { 128 | if dot.pos[0] < dot.size { 129 | dot.pos[0] = dot.size 130 | dot.dir[0] *= -1 131 | } 132 | if dot.pos[0] > width-dot.size { 133 | dot.pos[0] = width - dot.size 134 | dot.dir[0] *= -1 135 | } 136 | 137 | if dot.pos[1] < dot.size { 138 | dot.pos[1] = dot.size 139 | dot.dir[1] *= -1 140 | } 141 | 142 | if dot.pos[1] > height-dot.size { 143 | dot.pos[1] = height - dot.size 144 | dot.dir[1] *= -1 145 | } 146 | 147 | mdx := mousePos[0] - dot.pos[0] 148 | mdy := mousePos[1] - dot.pos[1] 149 | d := math.Sqrt(mdx*mdx + mdy*mdy) 150 | if d < 200 { 151 | dInv := 1 - d/200 152 | dot.dir[0] += (-mdx / d) * dInv * 8 153 | dot.dir[1] += (-mdy / d) * dInv * 8 154 | } 155 | for j, dot2 := range dt.dots { 156 | if i == j { 157 | continue 158 | } 159 | mx := dot2.pos[0] - dot.pos[0] 160 | my := dot2.pos[1] - dot.pos[1] 161 | d := math.Sqrt(mx*mx + my*my) 162 | if d < 100 { 163 | dInv := 1 - d/100 164 | dot.dir[0] += (-mx / d) * dInv 165 | dot.dir[1] += (-my / d) * dInv 166 | } 167 | } 168 | dot.dir[0] *= 0.1 //friction 169 | dot.dir[1] *= 0.1 //friction 170 | 171 | dot.pos[0] += dot.dir[0] * dt.speed * dtTime * 10 172 | dot.pos[1] += dot.dir[1] * dt.speed * dtTime * 10 173 | 174 | ctx.Set("globalAlpha", 0.5) 175 | ctx.Call("beginPath") 176 | ctx.Set("fillStyle", fmt.Sprintf("#%06x", dot.color)) 177 | ctx.Set("strokeStyle", fmt.Sprintf("#%06x", dot.color)) 178 | ctx.Set("lineWidth", dot.size) 179 | ctx.Call("arc", dot.pos[0], dot.pos[1], dot.size, 0, 2*math.Pi) 180 | ctx.Call("fill") 181 | 182 | } 183 | } 184 | 185 | // SetNDots reinitializes dots with n size 186 | func (dt *DotThing) SetNDots(n int) { 187 | dt.dots = make([]*Dot, n) 188 | for i := 0; i < n; i++ { 189 | dt.dots[i] = &Dot{ 190 | pos: [2]float64{ 191 | rand.Float64() * width, 192 | rand.Float64() * height, 193 | }, 194 | dir: [2]float64{ 195 | rand.NormFloat64(), 196 | rand.NormFloat64(), 197 | }, 198 | color: uint32(rand.Intn(0xFFFFFF)), 199 | size: 10, 200 | } 201 | } 202 | } 203 | 204 | // Dot represents a dot ... 205 | type Dot struct { 206 | pos [2]float64 207 | dir [2]float64 208 | color uint32 209 | size float64 210 | } 211 | -------------------------------------------------------------------------------- /vugu/root_vgen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Code generated by vugu via vugugen. Please regenerate instead of editing or add additional code in a separate file. DO NOT EDIT. 4 | 5 | import "fmt" 6 | import "reflect" 7 | import "github.com/vugu/vjson" 8 | import "github.com/vugu/vugu" 9 | import js "github.com/vugu/vugu/js" 10 | 11 | import "encoding/json" 12 | import "net/http" 13 | import "log" 14 | 15 | type Root struct { 16 | bpi bpi 17 | isLoading bool 18 | } 19 | 20 | type bpi struct { 21 | Time struct { 22 | Updated string `json:"updated"` 23 | } `json:"time"` 24 | BPI map[string]struct { 25 | Code string `json:"code"` 26 | Symbol string `json:"symbol"` 27 | RateFloat float64 `json:"rate_float"` 28 | } `json:"bpi"` 29 | } 30 | 31 | var c Root 32 | 33 | func (c *Root) HandleClick(event vugu.DOMEvent) { 34 | 35 | c.bpi = bpi{} 36 | 37 | go func(ee vugu.EventEnv) { 38 | 39 | ee.Lock() 40 | c.isLoading = true 41 | ee.UnlockRender() 42 | 43 | res, err := http.Get("https://api.coindesk.com/v1/bpi/currentprice.json") 44 | if err != nil { 45 | log.Printf("Error fetch()ing: %v", err) 46 | return 47 | } 48 | defer res.Body.Close() 49 | 50 | var newb bpi 51 | err = json.NewDecoder(res.Body).Decode(&newb) 52 | if err != nil { 53 | log.Printf("Error JSON decoding: %v", err) 54 | return 55 | } 56 | 57 | ee.Lock() 58 | defer ee.UnlockRender() 59 | c.bpi = newb 60 | c.isLoading = false 61 | 62 | }(event.EventEnv()) 63 | } 64 | 65 | func (c *Root) Build(vgin *vugu.BuildIn) (vgout *vugu.BuildOut) { 66 | 67 | vgout = &vugu.BuildOut{} 68 | 69 | var vgiterkey interface{} 70 | _ = vgiterkey 71 | var vgn *vugu.VGNode 72 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "div", Attr: []vugu.VGAttribute{vugu.VGAttribute{Namespace: "", Key: "class", Val: "demo-comp"}}} 73 | vgout.Out = append(vgout.Out, vgn) // root for output 74 | { 75 | vgparent := vgn 76 | _ = vgparent 77 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 78 | vgparent.AppendChild(vgn) 79 | if c.isLoading { 80 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "div", Attr: []vugu.VGAttribute(nil)} 81 | vgparent.AppendChild(vgn) 82 | { 83 | vgparent := vgn 84 | _ = vgparent 85 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "Loading..."} 86 | vgparent.AppendChild(vgn) 87 | } 88 | } 89 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 90 | vgparent.AppendChild(vgn) 91 | if len(c.bpi.BPI) > 0 { 92 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "div", Attr: []vugu.VGAttribute(nil)} 93 | vgparent.AppendChild(vgn) 94 | { 95 | vgparent := vgn 96 | _ = vgparent 97 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 98 | vgparent.AppendChild(vgn) 99 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "div", Attr: []vugu.VGAttribute(nil)} 100 | vgparent.AppendChild(vgn) 101 | { 102 | vgparent := vgn 103 | _ = vgparent 104 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "Updated: "} 105 | vgparent.AppendChild(vgn) 106 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "span", Attr: []vugu.VGAttribute(nil)} 107 | vgparent.AppendChild(vgn) 108 | vgn.SetInnerHTML(c.bpi.Time.Updated) 109 | } 110 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 111 | vgparent.AppendChild(vgn) 112 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "ul", Attr: []vugu.VGAttribute(nil)} 113 | vgparent.AppendChild(vgn) 114 | { 115 | vgparent := vgn 116 | _ = vgparent 117 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 118 | vgparent.AppendChild(vgn) 119 | for key, value := range c.bpi.BPI { 120 | var vgiterkey interface{} = key 121 | _ = vgiterkey 122 | key := key 123 | _ = key 124 | value := value 125 | _ = value 126 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "li", Attr: []vugu.VGAttribute(nil)} 127 | vgparent.AppendChild(vgn) 128 | { 129 | vgparent := vgn 130 | _ = vgparent 131 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 132 | vgparent.AppendChild(vgn) 133 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "span", Attr: []vugu.VGAttribute(nil)} 134 | vgparent.AppendChild(vgn) 135 | vgn.SetInnerHTML(key) 136 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: " "} 137 | vgparent.AppendChild(vgn) 138 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "span", Attr: []vugu.VGAttribute(nil)} 139 | vgparent.AppendChild(vgn) 140 | vgn.SetInnerHTML(fmt.Sprint(value.Symbol, value.RateFloat)) 141 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 142 | vgparent.AppendChild(vgn) 143 | } 144 | } 145 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 146 | vgparent.AppendChild(vgn) 147 | } 148 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 149 | vgparent.AppendChild(vgn) 150 | } 151 | } 152 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "} 153 | vgparent.AppendChild(vgn) 154 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "button", Attr: []vugu.VGAttribute(nil)} 155 | vgparent.AppendChild(vgn) 156 | vgn.DOMEventHandlerSpecList = append(vgn.DOMEventHandlerSpecList, vugu.DOMEventHandlerSpec{ 157 | EventType: "click", 158 | Func: func(event vugu.DOMEvent) { c.HandleClick(event) }, 159 | // TODO: implement capture, etc. mostly need to decide syntax 160 | }) 161 | { 162 | vgparent := vgn 163 | _ = vgparent 164 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "Fetch Bitcoin Price Index"} 165 | vgparent.AppendChild(vgn) 166 | } 167 | vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n"} 168 | vgparent.AppendChild(vgn) 169 | } 170 | return vgout 171 | } 172 | 173 | // 'fix' unused imports 174 | var _ fmt.Stringer 175 | var _ reflect.Type 176 | var _ vjson.RawMessage 177 | var _ js.Value 178 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 5 | github.com/chromedp/cdproto v0.0.0-20191009033829-c22f49c9ff0a/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= 6 | github.com/chromedp/chromedp v0.5.1/go.mod h1:3NMfuKTrKNr8PWEvHzdzZ57PK4jm9zW1C5nKiaWdxcM= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 h1:5e8X7WEdOWrjrKvgaWF6PRnDvJicfrkEnwAkWtMN74g= 11 | github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8/go.mod h1:tWboRRNagZwwwis4QIgEFG1ZNFwBJ3LAhSLAXAAxobQ= 12 | github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= 13 | github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= 14 | github.com/ebitengine/oto/v3 v3.2.0 h1:FuggTJTSI3/3hEYwZEIN0CZVXYT29ZOdCu+z/f4QjTw= 15 | github.com/ebitengine/oto/v3 v3.2.0/go.mod h1:dOKXShvy1EQbIXhXPFcKLargdnFqH0RjptecvyAxhyw= 16 | github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= 17 | github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 18 | github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984 h1:NwCC36eQsDf1xVZG9jD7ngXNNjsvk8KXky15ogA1Vo0= 19 | github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= 20 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= 21 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 22 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 23 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 24 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 25 | github.com/gopherjs/vecty v0.5.0 h1:pnaU0yIo0sT3WlDXi22WA9FNhwiswnHb+1mbS8wISW4= 26 | github.com/gopherjs/vecty v0.5.0/go.mod h1:Yd9AOfl6lN1BWnVnvaSBwKiTCAcnlIGGvdsODOOZ9RY= 27 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 28 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 29 | github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4= 30 | github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA= 31 | github.com/hajimehoshi/ebiten/v2 v2.7.2 h1:5HcWAjxhGMBocJh0jH/61Kx4QJ91HkzYtSeSucvVg7o= 32 | github.com/hajimehoshi/ebiten/v2 v2.7.2/go.mod h1:1vjyPw+h3n30rfTOpIsbWRXSxZ0Oz1cYc6Tq/2DKoQg= 33 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 34 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 35 | github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ= 36 | github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 37 | github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE= 38 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 39 | github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= 40 | github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= 41 | github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= 42 | github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 46 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 47 | github.com/slimsag/blackfriday v2.0.0+incompatible h1:3mjnT4p/+Ow8j8/T6H01S5pxXNPhVuOBzxmAbsszxi4= 48 | github.com/slimsag/blackfriday v2.0.0+incompatible/go.mod h1:K2UwInbJ1ywJkRQ2IMfjP071c3KZu7FiOn2NW97ckKk= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 51 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 52 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 53 | github.com/vugu/html v0.0.0-20190914200101-c62dc20b8289/go.mod h1:Y3pLGz8dZUSrB9SARXqFmtW8RNs4HIGAr0+JaWL31Vg= 54 | github.com/vugu/vjson v0.0.0-20200505061711-f9cbed27d3d9 h1:0cwYt2uGUAwxOYF6zAkVvCKWt8zOV3JhQqjvwKb6jf0= 55 | github.com/vugu/vjson v0.0.0-20200505061711-f9cbed27d3d9/go.mod h1:z7mAqSUjRDMQ09NIO18jG2llXMHLnUHlZ3/8MEMyBPA= 56 | github.com/vugu/vugu v0.3.5 h1:AdhL+RDWMI3ZLZij9b8HdRHTKqNYkSDbcgFND02zNGU= 57 | github.com/vugu/vugu v0.3.5/go.mod h1:E0NT8+F8KDmLDKzYTNySlcvljJdfOpBHzcMJ/0bYLfM= 58 | github.com/vugu/xxhash v0.0.0-20191111030615-ed24d0179019 h1:8NGiD5gWbVGObr+lnqcbM2rcOQBO6mr+m19BIblCdho= 59 | github.com/vugu/xxhash v0.0.0-20191111030615-ed24d0179019/go.mod h1:PrBK6+LJXwb+3EnJTHo43Uh4FhjFFwvN4jKk4Zc5zZ8= 60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 61 | golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= 62 | golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 63 | golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 64 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 65 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 66 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 67 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 68 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 72 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 75 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 78 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 79 | -------------------------------------------------------------------------------- /canvas/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ebitengine/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | _ "embed" 8 | "flag" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | _ "image/png" 13 | "log" 14 | "math" 15 | "math/rand" 16 | "time" 17 | 18 | "github.com/hajimehoshi/ebiten/v2" 19 | "github.com/hajimehoshi/ebiten/v2/audio" 20 | "github.com/hajimehoshi/ebiten/v2/audio/vorbis" 21 | "github.com/hajimehoshi/ebiten/v2/audio/wav" 22 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 23 | raudio "github.com/hajimehoshi/ebiten/v2/examples/resources/audio" 24 | "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" 25 | resources "github.com/hajimehoshi/ebiten/v2/examples/resources/images/flappy" 26 | "github.com/hajimehoshi/ebiten/v2/inpututil" 27 | "github.com/hajimehoshi/ebiten/v2/text/v2" 28 | ) 29 | 30 | var flagCRT = flag.Bool("crt", false, "enable the CRT effect") 31 | 32 | //go:embed crt.go 33 | var crtGo []byte 34 | 35 | func init() { 36 | rand.Seed(time.Now().UnixNano()) 37 | } 38 | 39 | func floorDiv(x, y int) int { 40 | d := x / y 41 | if d*y == x || x >= 0 { 42 | return d 43 | } 44 | return d - 1 45 | } 46 | 47 | func floorMod(x, y int) int { 48 | return x - floorDiv(x, y)*y 49 | } 50 | 51 | const ( 52 | screenWidth = 640 53 | screenHeight = 480 54 | tileSize = 32 55 | titleFontSize = fontSize * 1.5 56 | fontSize = 24 57 | smallFontSize = fontSize / 2 58 | pipeWidth = tileSize * 2 59 | pipeStartOffsetX = 8 60 | pipeIntervalX = 8 61 | pipeGapY = 5 62 | ) 63 | 64 | var ( 65 | gopherImage *ebiten.Image 66 | tilesImage *ebiten.Image 67 | arcadeFaceSource *text.GoTextFaceSource 68 | ) 69 | 70 | func init() { 71 | img, _, err := image.Decode(bytes.NewReader(resources.Gopher_png)) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | gopherImage = ebiten.NewImageFromImage(img) 76 | 77 | img, _, err = image.Decode(bytes.NewReader(resources.Tiles_png)) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | tilesImage = ebiten.NewImageFromImage(img) 82 | } 83 | 84 | func init() { 85 | s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.PressStart2P_ttf)) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | arcadeFaceSource = s 90 | } 91 | 92 | type Mode int 93 | 94 | const ( 95 | ModeTitle Mode = iota 96 | ModeGame 97 | ModeGameOver 98 | ) 99 | 100 | type Game struct { 101 | mode Mode 102 | 103 | // The gopher's position 104 | x16 int 105 | y16 int 106 | vy16 int 107 | 108 | // Camera 109 | cameraX int 110 | cameraY int 111 | 112 | // Pipes 113 | pipeTileYs []int 114 | 115 | gameoverCount int 116 | 117 | touchIDs []ebiten.TouchID 118 | gamepadIDs []ebiten.GamepadID 119 | 120 | audioContext *audio.Context 121 | jumpPlayer *audio.Player 122 | hitPlayer *audio.Player 123 | } 124 | 125 | func NewGame(crt bool) ebiten.Game { 126 | g := &Game{} 127 | g.init() 128 | if crt { 129 | return &GameWithCRTEffect{Game: g} 130 | } 131 | return g 132 | } 133 | 134 | func (g *Game) init() { 135 | g.x16 = 0 136 | g.y16 = 100 * 16 137 | g.cameraX = -240 138 | g.cameraY = 0 139 | g.pipeTileYs = make([]int, 256) 140 | for i := range g.pipeTileYs { 141 | g.pipeTileYs[i] = rand.Intn(6) + 2 142 | } 143 | 144 | if g.audioContext == nil { 145 | g.audioContext = audio.NewContext(48000) 146 | } 147 | 148 | jumpD, err := vorbis.DecodeWithoutResampling(bytes.NewReader(raudio.Jump_ogg)) 149 | if err != nil { 150 | log.Fatal(err) 151 | } 152 | g.jumpPlayer, err = g.audioContext.NewPlayer(jumpD) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | jabD, err := wav.DecodeWithoutResampling(bytes.NewReader(raudio.Jab_wav)) 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | g.hitPlayer, err = g.audioContext.NewPlayer(jabD) 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | } 166 | 167 | func (g *Game) isKeyJustPressed() bool { 168 | if inpututil.IsKeyJustPressed(ebiten.KeySpace) { 169 | return true 170 | } 171 | if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { 172 | return true 173 | } 174 | g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs[:0]) 175 | if len(g.touchIDs) > 0 { 176 | return true 177 | } 178 | g.gamepadIDs = ebiten.AppendGamepadIDs(g.gamepadIDs[:0]) 179 | for _, g := range g.gamepadIDs { 180 | if ebiten.IsStandardGamepadLayoutAvailable(g) { 181 | if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightBottom) { 182 | return true 183 | } 184 | if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightRight) { 185 | return true 186 | } 187 | } else { 188 | // The button 0/1 might not be A/B buttons. 189 | if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton0) { 190 | return true 191 | } 192 | if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton1) { 193 | return true 194 | } 195 | } 196 | } 197 | return false 198 | } 199 | 200 | func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { 201 | return screenWidth, screenHeight 202 | } 203 | 204 | func (g *Game) Update() error { 205 | switch g.mode { 206 | case ModeTitle: 207 | if g.isKeyJustPressed() { 208 | g.mode = ModeGame 209 | } 210 | case ModeGame: 211 | g.x16 += 32 212 | g.cameraX += 2 213 | if g.isKeyJustPressed() { 214 | g.vy16 = -96 215 | if err := g.jumpPlayer.Rewind(); err != nil { 216 | return err 217 | } 218 | g.jumpPlayer.Play() 219 | } 220 | g.y16 += g.vy16 221 | 222 | // Gravity 223 | g.vy16 += 4 224 | if g.vy16 > 96 { 225 | g.vy16 = 96 226 | } 227 | 228 | if g.hit() { 229 | if err := g.hitPlayer.Rewind(); err != nil { 230 | return err 231 | } 232 | g.hitPlayer.Play() 233 | g.mode = ModeGameOver 234 | g.gameoverCount = 30 235 | } 236 | case ModeGameOver: 237 | if g.gameoverCount > 0 { 238 | g.gameoverCount-- 239 | } 240 | if g.gameoverCount == 0 && g.isKeyJustPressed() { 241 | g.init() 242 | g.mode = ModeTitle 243 | } 244 | } 245 | return nil 246 | } 247 | 248 | func (g *Game) Draw(screen *ebiten.Image) { 249 | screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff}) 250 | g.drawTiles(screen) 251 | if g.mode != ModeTitle { 252 | g.drawGopher(screen) 253 | } 254 | 255 | var titleTexts string 256 | var texts string 257 | switch g.mode { 258 | case ModeTitle: 259 | titleTexts = "FLAPPY GOPHER" 260 | texts = "\n\n\n\n\n\nPRESS SPACE KEY\n\nOR A/B BUTTON\n\nOR TOUCH SCREEN" 261 | case ModeGameOver: 262 | texts = "\nGAME OVER!" 263 | } 264 | 265 | op := &text.DrawOptions{} 266 | op.GeoM.Translate(screenWidth/2, 3*titleFontSize) 267 | op.ColorScale.ScaleWithColor(color.White) 268 | op.LineSpacing = titleFontSize 269 | op.PrimaryAlign = text.AlignCenter 270 | text.Draw(screen, titleTexts, &text.GoTextFace{ 271 | Source: arcadeFaceSource, 272 | Size: titleFontSize, 273 | }, op) 274 | 275 | op = &text.DrawOptions{} 276 | op.GeoM.Translate(screenWidth/2, 3*titleFontSize) 277 | op.ColorScale.ScaleWithColor(color.White) 278 | op.LineSpacing = fontSize 279 | op.PrimaryAlign = text.AlignCenter 280 | text.Draw(screen, texts, &text.GoTextFace{ 281 | Source: arcadeFaceSource, 282 | Size: fontSize, 283 | }, op) 284 | 285 | if g.mode == ModeTitle { 286 | const msg = "Go Gopher by Renee French is\nlicenced under CC BY 3.0." 287 | 288 | op := &text.DrawOptions{} 289 | op.GeoM.Translate(screenWidth/2, screenHeight-smallFontSize/2) 290 | op.ColorScale.ScaleWithColor(color.White) 291 | op.LineSpacing = smallFontSize 292 | op.PrimaryAlign = text.AlignCenter 293 | op.SecondaryAlign = text.AlignEnd 294 | text.Draw(screen, msg, &text.GoTextFace{ 295 | Source: arcadeFaceSource, 296 | Size: smallFontSize, 297 | }, op) 298 | } 299 | 300 | op = &text.DrawOptions{} 301 | op.GeoM.Translate(screenWidth, 0) 302 | op.ColorScale.ScaleWithColor(color.White) 303 | op.LineSpacing = fontSize 304 | op.PrimaryAlign = text.AlignEnd 305 | text.Draw(screen, fmt.Sprintf("%04d", g.score()), &text.GoTextFace{ 306 | Source: arcadeFaceSource, 307 | Size: fontSize, 308 | }, op) 309 | 310 | ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.ActualTPS())) 311 | } 312 | 313 | func (g *Game) pipeAt(tileX int) (tileY int, ok bool) { 314 | if (tileX - pipeStartOffsetX) <= 0 { 315 | return 0, false 316 | } 317 | if floorMod(tileX-pipeStartOffsetX, pipeIntervalX) != 0 { 318 | return 0, false 319 | } 320 | idx := floorDiv(tileX-pipeStartOffsetX, pipeIntervalX) 321 | return g.pipeTileYs[idx%len(g.pipeTileYs)], true 322 | } 323 | 324 | func (g *Game) score() int { 325 | x := floorDiv(g.x16, 16) / tileSize 326 | if (x - pipeStartOffsetX) <= 0 { 327 | return 0 328 | } 329 | return floorDiv(x-pipeStartOffsetX, pipeIntervalX) 330 | } 331 | 332 | func (g *Game) hit() bool { 333 | if g.mode != ModeGame { 334 | return false 335 | } 336 | const ( 337 | gopherWidth = 30 338 | gopherHeight = 60 339 | ) 340 | w, h := gopherImage.Bounds().Dx(), gopherImage.Bounds().Dy() 341 | x0 := floorDiv(g.x16, 16) + (w-gopherWidth)/2 342 | y0 := floorDiv(g.y16, 16) + (h-gopherHeight)/2 343 | x1 := x0 + gopherWidth 344 | y1 := y0 + gopherHeight 345 | if y0 < -tileSize*4 { 346 | return true 347 | } 348 | if y1 >= screenHeight-tileSize { 349 | return true 350 | } 351 | xMin := floorDiv(x0-pipeWidth, tileSize) 352 | xMax := floorDiv(x0+gopherWidth, tileSize) 353 | for x := xMin; x <= xMax; x++ { 354 | y, ok := g.pipeAt(x) 355 | if !ok { 356 | continue 357 | } 358 | if x0 >= x*tileSize+pipeWidth { 359 | continue 360 | } 361 | if x1 < x*tileSize { 362 | continue 363 | } 364 | if y0 < y*tileSize { 365 | return true 366 | } 367 | if y1 >= (y+pipeGapY)*tileSize { 368 | return true 369 | } 370 | } 371 | return false 372 | } 373 | 374 | func (g *Game) drawTiles(screen *ebiten.Image) { 375 | const ( 376 | nx = screenWidth / tileSize 377 | ny = screenHeight / tileSize 378 | pipeTileSrcX = 128 379 | pipeTileSrcY = 192 380 | ) 381 | 382 | op := &ebiten.DrawImageOptions{} 383 | for i := -2; i < nx+1; i++ { 384 | // ground 385 | op.GeoM.Reset() 386 | op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), 387 | float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize))) 388 | screen.DrawImage(tilesImage.SubImage(image.Rect(0, 0, tileSize, tileSize)).(*ebiten.Image), op) 389 | 390 | // pipe 391 | if tileY, ok := g.pipeAt(floorDiv(g.cameraX, tileSize) + i); ok { 392 | for j := 0; j < tileY; j++ { 393 | op.GeoM.Reset() 394 | op.GeoM.Scale(1, -1) 395 | op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), 396 | float64(j*tileSize-floorMod(g.cameraY, tileSize))) 397 | op.GeoM.Translate(0, tileSize) 398 | var r image.Rectangle 399 | if j == tileY-1 { 400 | r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize) 401 | } else { 402 | r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2) 403 | } 404 | screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op) 405 | } 406 | for j := tileY + pipeGapY; j < screenHeight/tileSize-1; j++ { 407 | op.GeoM.Reset() 408 | op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), 409 | float64(j*tileSize-floorMod(g.cameraY, tileSize))) 410 | var r image.Rectangle 411 | if j == tileY+pipeGapY { 412 | r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize) 413 | } else { 414 | r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize) 415 | } 416 | screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op) 417 | } 418 | } 419 | } 420 | } 421 | 422 | func (g *Game) drawGopher(screen *ebiten.Image) { 423 | op := &ebiten.DrawImageOptions{} 424 | w, h := gopherImage.Bounds().Dx(), gopherImage.Bounds().Dy() 425 | op.GeoM.Translate(-float64(w)/2.0, -float64(h)/2.0) 426 | op.GeoM.Rotate(float64(g.vy16) / 96.0 * math.Pi / 6) 427 | op.GeoM.Translate(float64(w)/2.0, float64(h)/2.0) 428 | op.GeoM.Translate(float64(g.x16/16.0)-float64(g.cameraX), float64(g.y16/16.0)-float64(g.cameraY)) 429 | op.Filter = ebiten.FilterLinear 430 | screen.DrawImage(gopherImage, op) 431 | } 432 | 433 | type GameWithCRTEffect struct { 434 | ebiten.Game 435 | 436 | crtShader *ebiten.Shader 437 | } 438 | 439 | func (g *GameWithCRTEffect) DrawFinalScreen(screen ebiten.FinalScreen, offscreen *ebiten.Image, geoM ebiten.GeoM) { 440 | if g.crtShader == nil { 441 | s, err := ebiten.NewShader(crtGo) 442 | if err != nil { 443 | panic(fmt.Sprintf("flappy: failed to compiled the CRT shader: %v", err)) 444 | } 445 | g.crtShader = s 446 | } 447 | 448 | os := offscreen.Bounds().Size() 449 | 450 | op := &ebiten.DrawRectShaderOptions{} 451 | op.Images[0] = offscreen 452 | op.GeoM = geoM 453 | screen.DrawRectShader(os.X, os.Y, g.crtShader, op) 454 | } 455 | 456 | func main() { 457 | flag.Parse() 458 | ebiten.SetWindowSize(screenWidth, screenHeight) 459 | ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)") 460 | if err := ebiten.RunGame(NewGame(*flagCRT)); err != nil { 461 | panic(err) 462 | } 463 | } 464 | --------------------------------------------------------------------------------