├── LICENSE
├── README.md
├── arty
├── client
│ ├── build.sh
│ ├── index.html
│ ├── main.go
│ ├── main.wasm
│ └── wasm_exec.js
├── painter
│ ├── assets
│ │ └── font.ttf
│ ├── font
│ │ └── font.go
│ ├── fontcache.go
│ ├── operations.go
│ └── painter.go
└── server
│ └── main.go
├── bittune
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── bouncy
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── bumpy
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── go.mod
├── go.sum
├── hexy
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── rainbow-mouse
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── repulsion
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
├── serve.go
└── splashy
├── build.sh
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go webassembly experiments
2 |
3 | Requires go1.13
4 |
5 | - [bouncy](https://stdiopt.github.io/gowasm-experiments/bouncy)
6 | - [rainbow-mouse](https://stdiopt.github.io/gowasm-experiments/rainbow-mouse)
7 | - [repulsion](https://stdiopt.github.io/gowasm-experiments/repulsion)
8 | - [bumpy](https://stdiopt.github.io/gowasm-experiments/bumpy)
9 | - [splashy](https://stdiopt.github.io/gowasm-experiments/splashy)
10 | - [arty](https://stdiopt.github.io/gowasm-experiments/arty/client)
11 | - [hexy](https://stdiopt.github.io/gowasm-experiments/hexy)
12 | - [bittune](https://stdiopt.github.io/gowasm-experiments/bittune)
13 |
14 | ## Building and running
15 |
16 | ```sh
17 | $ cd {proj} # sub folder (i.e. bouncy, rainbow-mouse)
18 | $ go get -v # ignore the js warning
19 | $ ./build.sh
20 | $ go run ../serve.go
21 | ```
22 |
23 | Serve with caddy or anything else that is able to set the mimetype
24 | 'application/wasm' for .wasm files
25 |
--------------------------------------------------------------------------------
/arty/client/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/arty/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - canvas
4 |
5 |
21 |
53 |
54 |
55 |
56 |
57 |
58 | connecting...
59 |
60 |
61 | color
62 |
63 |
64 | size 6
65 |
66 |
[source code]
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/arty/client/main.go:
--------------------------------------------------------------------------------
1 | //Wasming
2 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "image/color"
8 | "log"
9 | "strconv"
10 | "syscall/js"
11 |
12 | "github.com/stdiopt/gowasm-experiments/arty/painter"
13 |
14 | "github.com/lucasb-eyer/go-colorful"
15 | )
16 |
17 | func main() {
18 | c, err := NewCanvasClient("wss:/arty.us.hexasoftware.com")
19 | if err != nil {
20 | log.Fatal("could not start", err)
21 | }
22 | defer c.Close()
23 | c.Start()
24 | }
25 |
26 | type pos struct {
27 | x, y float64
28 | }
29 |
30 | type CanvasClient struct {
31 | done chan struct{}
32 | painter *painter.BufPainter
33 | addr string
34 |
35 | doc js.Value
36 | canvasEl js.Value
37 | ctx js.Value
38 | ws js.Value
39 | im js.Value
40 | // will hold js part of the image
41 | byteArray js.Value
42 |
43 | colorHex string
44 | lineWidth float64
45 |
46 | textOff pos
47 | lastPos pos
48 | width float64
49 | height float64
50 | }
51 |
52 | func NewCanvasClient(addr string) (*CanvasClient, error) {
53 | done := make(chan struct{})
54 |
55 | painter, err := painter.New()
56 | if err != nil {
57 | return nil, err
58 | }
59 | return &CanvasClient{
60 | done: done,
61 | painter: painter,
62 | addr: addr,
63 | lineWidth: 10,
64 | }, nil
65 | }
66 |
67 | func (c *CanvasClient) Start() {
68 | c.initCanvas()
69 | c.initFrameUpdate()
70 | c.initConnection()
71 |
72 | <-c.done
73 | }
74 |
75 | func (c *CanvasClient) Close() {
76 | close(c.done)
77 | }
78 |
79 | func (c *CanvasClient) initCanvas() {
80 | c.doc = js.Global().Get("document")
81 | c.canvasEl = c.doc.Call("getElementById", "mycanvas")
82 | c.width = c.canvasEl.Get("width").Float()
83 | c.height = c.canvasEl.Get("height").Float()
84 | c.ctx = c.canvasEl.Call("getContext", "2d")
85 | c.im = c.ctx.Call("createImageData", 1, 1)
86 | c.byteArray = js.Global().Get("Uint8Array").New(1 * 4)
87 | c.painter.OnInit = func(m painter.InitOP) {
88 | c.im = c.ctx.Call("createImageData", m.Width, m.Height)
89 | c.byteArray = js.Global().Get("Uint8Array").New(m.Width * m.Height * 4)
90 | c.SetStatus("connected")
91 | c.initEvents()
92 | }
93 | }
94 |
95 | func (c *CanvasClient) initFrameUpdate() {
96 | // Hold the callbacks without blocking
97 | go func() {
98 | var renderFrame js.Func
99 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
100 | c.draw()
101 | js.Global().Call("requestAnimationFrame", renderFrame)
102 | return nil
103 | })
104 | defer renderFrame.Release()
105 | js.Global().Call("requestAnimationFrame", renderFrame)
106 | <-c.done
107 | }()
108 | }
109 |
110 | func (c *CanvasClient) initConnection() {
111 | go func() {
112 | c.SetStatus("connecting...")
113 | c.ws = js.Global().Get("WebSocket").New(c.addr)
114 | onopen := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
115 | c.SetStatus("receiving... (it takes some time)")
116 | return nil
117 | })
118 | defer onopen.Release()
119 | onmessage := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
120 | c.painter.HandleRaw([]byte(args[0].Get("data").String()))
121 | return nil
122 | })
123 | defer onmessage.Release()
124 | c.ws.Set("onopen", onopen)
125 | c.ws.Set("onmessage", onmessage)
126 |
127 | <-c.done
128 | }()
129 | }
130 | func (c *CanvasClient) initEvents() {
131 | go func() {
132 | // DOM events
133 | colorEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
134 | e := args[0]
135 | c.colorHex = e.Get("target").Get("value").String()
136 | return nil
137 | })
138 | szEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
139 | e := args[0]
140 | v, _ := strconv.ParseFloat(e.Get("target").Get("value").String(), 64)
141 | c.lineWidth = v
142 | return nil
143 | })
144 | defer szEvt.Release()
145 |
146 | c.doc.Call("getElementById", "color").Call("addEventListener", "change", colorEvt)
147 | c.doc.Call("getElementById", "size").Call("addEventListener", "change", szEvt)
148 |
149 | // Input events
150 | mouseDown := false
151 | mouseDownEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
152 | e := args[0]
153 | if e.Get("target") != c.canvasEl || e.Get("buttons").Float() != 1 {
154 | return nil
155 | }
156 | mouseDown = true
157 | if !e.Get("shiftKey").Bool() {
158 | c.lastPos.x = e.Get("pageX").Float()
159 | c.lastPos.y = e.Get("pageY").Float()
160 | c.textOff = pos{} // reset
161 | return nil
162 | }
163 | c.drawAtPointer(e)
164 | return nil
165 | })
166 | defer mouseDownEvt.Release()
167 |
168 | mouseUpEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
169 | mouseDown = false
170 | return nil
171 | })
172 | defer mouseUpEvt.Release()
173 |
174 | mouseMoveEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
175 | if !mouseDown {
176 | return nil
177 | }
178 | c.drawAtPointer(args[0])
179 | return nil
180 | })
181 |
182 | keyPressEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
183 | e := args[0]
184 | e.Call("preventDefault")
185 | key := e.Get("key").String()
186 | if key == "Enter" {
187 | c.textOff.x = 0
188 | c.textOff.y += (c.lineWidth + 10)
189 | }
190 | if len(key) != 1 {
191 | return nil
192 | }
193 | col, _ := colorful.Hex(c.colorHex) // Ignore error
194 | op := painter.TextOP{
195 | color.RGBA{uint8(col.R * 255), uint8(col.G * 255), uint8(col.B * 255), 255},
196 | c.lineWidth + 6,
197 | c.lastPos.x + c.textOff.x, c.lastPos.y + c.textOff.y,
198 | key,
199 | }
200 | c.textOff.x += (c.lineWidth + 10) * 0.6
201 |
202 | c.painter.HandleOP(op)
203 | buf, err := json.Marshal(painter.Message{op})
204 | if err != nil {
205 | return nil
206 | }
207 | c.ws.Call("send", string(buf))
208 | return nil
209 |
210 | })
211 | defer keyPressEvt.Release()
212 | c.doc.Call("addEventListener", "mousemove", mouseMoveEvt)
213 | c.doc.Call("addEventListener", "mousedown", mouseDownEvt)
214 | c.doc.Call("addEventListener", "mouseup", mouseUpEvt)
215 | c.doc.Call("addEventListener", "keypress", keyPressEvt)
216 |
217 | <-c.done
218 | }()
219 | }
220 | func (c *CanvasClient) drawAtPointer(e js.Value) {
221 | lastPos := c.lastPos
222 |
223 | c.lastPos.x = e.Get("pageX").Float()
224 | c.lastPos.y = e.Get("pageY").Float()
225 |
226 | col, _ := colorful.Hex(c.colorHex) // Ignore error
227 | op := painter.LineOP{
228 | color.RGBA{uint8(col.R * 255), uint8(col.G * 255), uint8(col.B * 255), 255},
229 | c.lineWidth,
230 | lastPos.x, lastPos.y,
231 | c.lastPos.x, c.lastPos.y,
232 | }
233 | c.painter.HandleOP(op)
234 |
235 | buf, err := json.Marshal(painter.Message{op})
236 | if err != nil {
237 | return
238 | }
239 | c.ws.Call("send", string(buf))
240 |
241 | }
242 | func (c *CanvasClient) SetStatus(txt string) {
243 | c.doc.Call("getElementById", "status").Set("innerHTML", txt)
244 | }
245 | func (c *CanvasClient) draw() {
246 | // golang buffer
247 | // Needs to be a Uint8Array while image data have Uint8ClampedArray
248 | js.CopyBytesToJS(c.byteArray, c.painter.ImageData())
249 | c.im.Get("data").Call("set", c.byteArray)
250 | c.ctx.Call("putImageData", c.im, 0, 0)
251 | }
252 |
--------------------------------------------------------------------------------
/arty/client/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/arty/client/main.wasm
--------------------------------------------------------------------------------
/arty/client/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/arty/painter/assets/font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/arty/painter/assets/font.ttf
--------------------------------------------------------------------------------
/arty/painter/fontcache.go:
--------------------------------------------------------------------------------
1 | package painter
2 |
3 | import (
4 | "github.com/golang/freetype/truetype"
5 | "github.com/llgcode/draw2d"
6 | )
7 |
8 | type FontCache map[string]*truetype.Font
9 |
10 | func (f FontCache) Load(fd draw2d.FontData) (*truetype.Font, error) {
11 | font, ok := f[fd.Name]
12 | if !ok {
13 | return f["roboto"], nil
14 | }
15 | return font, nil
16 | }
17 |
18 | func (f *FontCache) Store(fd draw2d.FontData, tf *truetype.Font) {
19 | (*f)[fd.Name] = tf
20 | }
21 |
--------------------------------------------------------------------------------
/arty/painter/operations.go:
--------------------------------------------------------------------------------
1 | package painter
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "image/color"
7 | )
8 |
9 | const (
10 | opInit = iota + 1
11 | opLine
12 | opText
13 | )
14 |
15 | // OP Wrapper
16 | type Message struct {
17 | Payload interface{}
18 | }
19 |
20 | func (m *Message) UnmarshalJSON(raw []byte) error {
21 | v := struct {
22 | OP uint
23 | Payload json.RawMessage
24 | }{}
25 | err := json.Unmarshal(raw, &v)
26 | if err != nil {
27 | return err
28 | }
29 | switch v.OP {
30 | case opInit:
31 | payload := InitOP{}
32 | err = json.Unmarshal(v.Payload, &payload)
33 | m.Payload = payload
34 | case opLine:
35 | payload := LineOP{}
36 | err = json.Unmarshal(v.Payload, &payload)
37 | m.Payload = payload
38 | case opText:
39 | payload := TextOP{}
40 | err = json.Unmarshal(v.Payload, &payload)
41 | m.Payload = payload
42 | default:
43 | return errors.New("unknown operation")
44 | }
45 | return err
46 | }
47 | func (m Message) MarshalJSON() ([]byte, error) {
48 | v := struct {
49 | OP uint
50 | Payload interface{}
51 | }{
52 | Payload: m.Payload,
53 | }
54 | switch m.Payload.(type) {
55 | case InitOP:
56 | v.OP = opInit
57 | case LineOP:
58 | v.OP = opLine
59 | case TextOP:
60 | v.OP = opText
61 | }
62 | return json.Marshal(v)
63 | }
64 |
65 | // This ops will be marshalled with the wrapper struct
66 | type InitOP struct {
67 | Width, Height int
68 | Data []byte
69 | }
70 |
71 | type LineOP struct {
72 | Color color.RGBA
73 | Width float64
74 | X1, Y1 float64
75 | X2, Y2 float64
76 | }
77 |
78 | type TextOP struct {
79 | Color color.RGBA
80 | Size float64
81 | X, Y float64
82 | Text string
83 | }
84 |
--------------------------------------------------------------------------------
/arty/painter/painter.go:
--------------------------------------------------------------------------------
1 | //go:generate go get github.com/gohxs/folder2go
2 | //go:generate folder2go -nobackup assets font
3 |
4 | package painter
5 |
6 | import (
7 | "encoding/json"
8 | "errors"
9 | "image"
10 |
11 | "github.com/golang/freetype/truetype"
12 | "github.com/llgcode/draw2d"
13 | "github.com/llgcode/draw2d/draw2dimg"
14 | "github.com/stdiopt/gowasm-experiments/arty/painter/font"
15 | )
16 |
17 | type BufPainter struct {
18 | image *image.RGBA
19 | ctx *draw2dimg.GraphicContext
20 | font *truetype.Font
21 | fontData draw2d.FontData
22 | OnInit func(InitOP)
23 | }
24 |
25 | func New() (*BufPainter, error) {
26 | font, err := truetype.Parse(font.Data["font.ttf"])
27 | if err != nil {
28 | return nil, err
29 | }
30 | return &BufPainter{font: font}, nil
31 | }
32 |
33 | func (p *BufPainter) HandleRaw(msg []byte) error {
34 | m := Message{}
35 | err := json.Unmarshal(msg, &m)
36 | if err != nil {
37 | return err
38 | }
39 | return p.HandleOP(m.Payload)
40 | }
41 |
42 | func (p *BufPainter) HandleOP(op interface{}) error {
43 | switch o := op.(type) {
44 | case InitOP:
45 | p.Init(o)
46 | case LineOP:
47 | p.Line(o)
48 | case TextOP:
49 | p.Text(o)
50 | default:
51 | return errors.New("unknown op")
52 | }
53 | return nil
54 | }
55 |
56 | func (p *BufPainter) Set(buf []byte) {
57 | if buf == nil {
58 | return
59 | }
60 | copy(p.image.Pix, buf)
61 | }
62 |
63 | func (p *BufPainter) ImageData() []byte {
64 | if p.image == nil {
65 | return nil
66 | }
67 | return p.image.Pix
68 | }
69 |
70 | func (p *BufPainter) Width() int {
71 | return p.image.Bounds().Max.X
72 | }
73 |
74 | func (p *BufPainter) Height() int {
75 | return p.image.Bounds().Max.Y
76 | }
77 |
78 | func (p *BufPainter) Init(op InitOP) {
79 | p.image = image.NewRGBA(image.Rect(0, 0, op.Width, op.Height))
80 |
81 | // init font
82 | fontData := draw2d.FontData{
83 | Name: "roboto",
84 | Family: draw2d.FontFamilySans,
85 | Style: draw2d.FontStyleNormal,
86 | }
87 | fontCache := &FontCache{}
88 | fontCache.Store(fontData, p.font)
89 |
90 | p.ctx = draw2dimg.NewGraphicContext(p.image)
91 | p.ctx.FontCache = fontCache
92 |
93 | p.Set(op.Data) // Image
94 | if p.OnInit != nil {
95 | p.OnInit(op)
96 | }
97 | }
98 | func (p *BufPainter) Line(op LineOP) {
99 | c := p.ctx
100 | c.SetStrokeColor(op.Color)
101 | c.SetLineWidth(op.Width)
102 | c.BeginPath()
103 | c.MoveTo(op.X1, op.Y1)
104 | c.LineTo(op.X2, op.Y2)
105 | c.Stroke()
106 | }
107 | func (p *BufPainter) Text(op TextOP) {
108 | c := p.ctx
109 | c.SetFillColor(op.Color)
110 | c.SetFont(p.font)
111 | c.SetFontSize(op.Size)
112 | c.FillStringAt(op.Text, op.X, op.Y)
113 | }
114 |
--------------------------------------------------------------------------------
/arty/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "image/color"
5 | "log"
6 | "net/http"
7 | "sync"
8 |
9 | "github.com/stdiopt/gowasm-experiments/arty/painter"
10 |
11 | "github.com/gorilla/websocket"
12 | )
13 |
14 | func main() {
15 | addr := ":4444"
16 | log.Println("Listening at ", addr)
17 | server, err := NewCanvasServer(1920, 1080)
18 | if err != nil {
19 | log.Fatal("error starting canvas server", err)
20 | }
21 | http.ListenAndServe(addr, server)
22 | }
23 |
24 | type CanvasServer struct {
25 | painter *painter.BufPainter
26 | clients sync.Map
27 | }
28 |
29 | func NewCanvasServer(w, h int) (*CanvasServer, error) {
30 | p, err := painter.New()
31 | if err != nil {
32 | return nil, err
33 | }
34 | p.Init(painter.InitOP{Width: w, Height: h})
35 |
36 | p.HandleOP(painter.TextOP{
37 | Color: color.RGBA{R: 0, G: 0, B: 0, A: 255},
38 | X: 10.0,
39 | Y: 10.0,
40 | Text: "Hello world",
41 | })
42 | return &CanvasServer{p, sync.Map{}}, nil
43 | }
44 |
45 | var upgrader = websocket.Upgrader{
46 | CheckOrigin: func(r *http.Request) bool { return true },
47 | }
48 |
49 | func (s *CanvasServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
50 | log.Println("Receiving connection from:", r.RemoteAddr)
51 | c, err := upgrader.Upgrade(w, r, nil)
52 | if err != nil {
53 | log.Println("upgrade err", err)
54 | return
55 | }
56 |
57 | c.WriteJSON(painter.Message{painter.InitOP{
58 | Width: s.painter.Width(),
59 | Height: s.painter.Height(),
60 | Data: s.painter.ImageData(),
61 | }})
62 | if err != nil {
63 | log.Println("sending msg error")
64 | return
65 | }
66 |
67 | ncli := &Cli{conn: c}
68 | s.clients.Store(ncli, true)
69 | defer func() {
70 | c.Close()
71 | s.clients.Delete(ncli)
72 | }()
73 | for {
74 | mt, message, err := c.ReadMessage()
75 | if err != nil {
76 | log.Println("Bye bye", r.RemoteAddr)
77 | return
78 | }
79 | if mt != websocket.TextMessage {
80 | continue
81 | }
82 | // draw in server
83 | err = s.painter.HandleRaw(message)
84 | if err != nil {
85 | log.Println("what?", err)
86 | }
87 |
88 | // Broadcast to other clients
89 | s.clients.Range(func(key, value interface{}) bool {
90 | cl := key.(*Cli)
91 | if cl == ncli {
92 | return true
93 | }
94 | err := cl.send(mt, message)
95 | if err != nil {
96 | log.Println("Erro: sending to cli", err)
97 | }
98 | return true
99 | })
100 |
101 | }
102 | }
103 |
104 | // Cli concurrent safe client
105 | type Cli struct {
106 | sync.Mutex
107 | conn *websocket.Conn
108 | }
109 |
110 | func (c *Cli) send(mt int, msg []byte) error {
111 | c.Lock()
112 | defer c.Unlock()
113 | return c.conn.WriteMessage(mt, msg)
114 | }
115 |
--------------------------------------------------------------------------------
/bittune/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/bittune/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | go webassembly - bittune
5 |
15 |
16 |
22 |
23 |
24 | Using Web Audio API
25 |
26 |
27 | play
28 |
29 | 80 bpm
30 |
31 | 32
32 |
33 | [source]
34 |
35 |
36 |
--------------------------------------------------------------------------------
/bittune/main.go:
--------------------------------------------------------------------------------
1 | // +build js,wasm
2 |
3 | package main
4 |
5 | import (
6 | "encoding/base64"
7 | "fmt"
8 | "math/rand"
9 | "strconv"
10 | "syscall/js"
11 | "time"
12 | )
13 |
14 | func main() {
15 | t := &audioThing{}
16 | t.Start()
17 | }
18 |
19 | type dom struct {
20 | doc js.Value
21 | beat js.Value
22 | bpm js.Value
23 | bpmLbl js.Value
24 | tlen js.Value
25 | tlenLbl js.Value
26 | }
27 |
28 | type audioThing struct {
29 | ctx js.Value
30 | el dom
31 |
32 | ins []func()
33 | seq []bool
34 | curStep int
35 |
36 | BPM byte
37 | trackLen byte
38 |
39 | playing bool
40 | done chan struct{}
41 |
42 | wn js.Value
43 | }
44 |
45 | func (t *audioThing) Start() {
46 |
47 | t.done = make(chan struct{}, 0)
48 |
49 | doc := js.Global().Get("document")
50 | actx := js.Global().Get("AudioContext")
51 | if !actx.Truthy() {
52 | actx = js.Global().Get("webkitAudioContext") // safari
53 | }
54 |
55 | t.ctx = actx.New()
56 |
57 | t.el.beat = doc.Call("getElementById", "beat")
58 | t.el.bpm = doc.Call("getElementById", "bpm")
59 | t.el.bpmLbl = t.el.bpm.Get("nextElementSibling")
60 | t.el.tlen = doc.Call("getElementById", "tlen")
61 | t.el.tlenLbl = t.el.tlen.Get("nextElementSibling")
62 |
63 | notes := []float64{
64 | 523.3, 554.4, 587.3, 622.3, 659.3, 698.5,
65 | 740.0, 784.0, 830.6, 880.0, 932.3, 987.8,
66 | }
67 | // Initialize instruments
68 | t.ins = []func(){
69 | t.playKick,
70 | t.playSnare,
71 | t.playCHithat,
72 | }
73 | // Add tune
74 | for i := 0; i <= 12; i++ {
75 | n := notes[i%len(notes)] * (1 + float64((i / len(notes))))
76 | t.ins = append(t.ins, t.createTune(n))
77 | }
78 | bufSize := 4096
79 | t.wn = t.ctx.Call("createScriptProcessor", bufSize, 1, 1)
80 | whiteNoiseFn := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
81 | e := args[0]
82 | out := e.Get("outputBuffer").Call("getChannelData", 0)
83 | for i := 0; i < bufSize; i++ {
84 | out.SetIndex(i, rand.Float64()*2-1)
85 | }
86 | return nil
87 | })
88 | defer whiteNoiseFn.Release()
89 | t.wn.Set("onaudioprocess", whiteNoiseFn)
90 |
91 | t.setBPM(80)
92 | t.setTrackLen(32)
93 |
94 | go t.handleEvents()
95 | t.hashRestore()
96 |
97 | <-t.done
98 | }
99 |
100 | func (t *audioThing) play() {
101 | if t.playing {
102 | return
103 | }
104 | t.playing = true
105 |
106 | go func() {
107 | for {
108 | beat := time.After(time.Minute / time.Duration(t.BPM) / 4)
109 | select {
110 | case <-t.done:
111 | return
112 | case <-beat:
113 | t.step()
114 | }
115 | }
116 | }()
117 | }
118 |
119 | // Build beats DOM
120 | func (t *audioThing) buildDOM() {
121 | beatHTML := ""
122 | for i := 0; i < len(t.seq)/len(t.ins); i++ {
123 | stepHTML := ""
124 | for j := 0; j < len(t.ins); j++ {
125 | stepHTML += fmt.Sprintf(
126 | `
`,
127 | i*len(t.ins)+j,
128 | )
129 | }
130 | beatHTML += fmt.Sprintf(`%s
`, stepHTML)
131 | }
132 | t.el.beat.Set("innerHTML", beatHTML)
133 | }
134 |
135 | func (t *audioThing) setTrackLen(n byte) {
136 | t.curStep = 0
137 | t.trackLen = n
138 | t.el.tlenLbl.Set("innerHTML", fmt.Sprint(n))
139 | t.el.tlen.Set("value", fmt.Sprint(n))
140 |
141 | t.seq = make([]bool, int(n)*len(t.ins))
142 | t.buildDOM()
143 | }
144 | func (t *audioThing) setBPM(n byte) {
145 | t.BPM = n
146 | t.el.bpmLbl.Set("innerHTML", fmt.Sprintf("%d bpm", n))
147 | t.el.bpm.Set("value", n)
148 | }
149 |
150 | // Set DOM events
151 | func (t *audioThing) handleEvents() {
152 | // Handle document click
153 | handleClick := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
154 | ev := args[0]
155 | target := ev.Get("target")
156 | if target.Call("matches", "#play").Bool() {
157 | t.play()
158 | target.Call("setAttribute", "disabled", "disabled")
159 | return nil
160 | }
161 | if !target.Call("matches", ".key").Bool() {
162 | return nil
163 | }
164 | keyIs := ev.Get("target").Call("getAttribute", "key").String()
165 | keyI, err := strconv.Atoi(keyIs)
166 | if err != nil {
167 | println("wrong key", keyIs)
168 | return nil
169 | }
170 | t.seq[keyI] = !t.seq[keyI]
171 | ev.Get("target").Get("classList").Call("toggle", "active", t.seq[keyI])
172 | t.hashStore()
173 | return nil
174 | })
175 | defer handleClick.Release()
176 | js.Global().Call("addEventListener", "click", handleClick)
177 |
178 | // handle location hashChange
179 | handleHashChange := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
180 | t.hashRestore()
181 | return nil
182 | })
183 | defer handleHashChange.Release()
184 | js.Global().Call("addEventListener", "hashchange", handleHashChange)
185 |
186 | // handle bpm input
187 | handleBpmInput := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
188 | e := args[0]
189 | sval := e.Get("target").Get("value").String()
190 | bpm, err := strconv.Atoi(sval)
191 | if err != nil {
192 | println("wrong input value")
193 | return nil
194 | }
195 |
196 | t.setBPM(byte(bpm))
197 | t.hashStore()
198 | return nil
199 | })
200 | defer handleBpmInput.Release()
201 | t.el.bpm.Call("addEventListener", "input", handleBpmInput)
202 |
203 | // handle track length input
204 | handleTrackLenInput := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
205 | e := args[0]
206 | sval := e.Get("target").Get("value").String()
207 | tlen, err := strconv.Atoi(sval)
208 | if err != nil {
209 | println("wrong input value")
210 | return nil
211 | }
212 | t.setTrackLen(byte(tlen))
213 | t.hashStore()
214 | return nil
215 | })
216 | defer handleTrackLenInput.Release()
217 | t.el.tlen.Call("addEventListener", "input", handleTrackLenInput)
218 |
219 | <-t.done
220 | }
221 |
222 | func (t *audioThing) step() {
223 | t.el.beat.
224 | Get("children").
225 | Index(t.curStep).
226 | Get("classList").
227 | Call("toggle", "current", false)
228 | t.curStep = (t.curStep + 1) % int(t.trackLen)
229 | t.el.beat.
230 | Get("children").
231 | Index(t.curStep).
232 | Get("classList").
233 | Call("toggle", "current", true)
234 | for i := range t.ins {
235 | if t.seq[t.curStep*len(t.ins)+i] {
236 | t.ins[i]()
237 | }
238 | }
239 | }
240 |
241 | func (t *audioThing) hashStore() {
242 | var bitbuf = make([]byte, len(t.seq)/8+3)
243 | bitbuf[0] = t.BPM
244 | bitbuf[1] = t.trackLen
245 | bcur := 1
246 | for i := range t.seq {
247 | if i&7 == 0 {
248 | bcur++
249 | }
250 | v := 0
251 | if t.seq[i] {
252 | v = 1
253 | }
254 | bitbuf[bcur] |= byte(v << uint(7-(i&7)))
255 | }
256 | b64 := base64.StdEncoding.EncodeToString(bitbuf)
257 | js.Global().Get("history").Call("pushState", b64, "", "#"+b64)
258 | }
259 |
260 | func (t *audioThing) hashRestore() {
261 | hash := js.Global().Get("location").Get("hash").String()
262 | if len(hash) == 0 {
263 | return
264 | }
265 | hash = hash[1:] // skip '#'
266 | bitbuf, err := base64.StdEncoding.DecodeString(hash)
267 | if err != nil {
268 | fmt.Println("wrong hash", err)
269 | return
270 | }
271 | t.setBPM(bitbuf[0])
272 | if bitbuf[1] != t.trackLen {
273 | t.setTrackLen(bitbuf[1])
274 | }
275 | bitbuf = bitbuf[2:]
276 | steps := []bool{}
277 |
278 | doc := js.Global().Get("document")
279 | for i := 0; i < len(bitbuf); i++ {
280 | for j := 7; j >= 0; j-- {
281 | v := ((bitbuf[i] >> uint(j)) & 1) != 0
282 | steps = append(steps, v)
283 | el := doc.Call("querySelector",
284 | fmt.Sprintf(`[key="%d"]`, len(steps)-1),
285 | )
286 | if !el.Truthy() {
287 | continue
288 | }
289 | el.Get("classList").Call("toggle", "active", v)
290 | }
291 | }
292 | t.seq = steps
293 | }
294 |
295 | // Improve this
296 | func (t *audioThing) playKick() {
297 | currentTime := t.ctx.Get("currentTime").Float()
298 |
299 | g := t.ctx.Call("createGain")
300 | g.Call("connect", t.ctx.Get("destination"))
301 | g.Get("gain").Set("value", 0.4)
302 | g.Get("gain").Call("linearRampToValueAtTime", 0, currentTime+0.05)
303 |
304 | f := t.ctx.Call("createBiquadFilter")
305 | f.Call("connect", g)
306 | f.Set("type", "lowpass")
307 | f.Get("frequency").Set("value", 400)
308 | f.Get("frequency").Call("linearRampToValueAtTime", 1, currentTime+0.1)
309 |
310 | o := t.ctx.Call("createOscillator")
311 | o.Call("connect", g)
312 | o.Set("type", "sine")
313 | o.Get("frequency").Set("value", 40)
314 |
315 | var ended js.Func
316 | ended = js.FuncOf(func(t js.Value, args []js.Value) interface{} {
317 | g.Call("disconnect")
318 | ended.Release()
319 | return nil
320 | })
321 | o.Set("onended", ended)
322 | o.Call("start")
323 | o.Call("stop", currentTime+0.05)
324 | }
325 |
326 | func (t *audioThing) playSnare() {
327 | currentTime := t.ctx.Get("currentTime").Float()
328 |
329 | g := t.ctx.Call("createGain")
330 | g.Call("connect", t.ctx.Get("destination"))
331 | g.Get("gain").Set("value", 0.3)
332 | g.Get("gain").Call("linearRampToValueAtTime", 0, currentTime+0.1)
333 |
334 | f := t.ctx.Call("createBiquadFilter")
335 | f.Call("connect", g)
336 | f.Set("type", "bandpass")
337 | f.Get("frequency").Set("value", 500)
338 | f.Get("frequency").Call("linearRampToValueAtTime", 1500, currentTime+0.02)
339 |
340 | o := t.ctx.Call("createOscillator")
341 | o.Call("connect", f)
342 | o.Set("type", "sine")
343 | o.Get("frequency").Set("value", 220)
344 | o.Get("frequency").Call("linearRampToValueAtTime", 10, currentTime+0.1)
345 |
346 | var ended js.Func
347 | ended = js.FuncOf(func(t js.Value, args []js.Value) interface{} {
348 | g.Call("disconnect")
349 | ended.Release()
350 | return nil
351 | })
352 | o.Set("onended", ended)
353 | o.Call("start")
354 | o.Call("stop", currentTime+0.1)
355 |
356 | g2 := t.ctx.Call("createGain")
357 | g2.Call("connect", f)
358 | g2.Get("gain").Set("value", 0.1)
359 | g2.Get("gain").Call("linearRampToValueAtTime", 0.2, currentTime+0.001)
360 |
361 | t.wn.Call("connect", g2)
362 |
363 | }
364 |
365 | func (t *audioThing) playCHithat() {
366 | currentTime := t.ctx.Get("currentTime").Float()
367 | g := t.ctx.Call("createGain")
368 | g.Call("connect", t.ctx.Get("destination"))
369 | g.Get("gain").Set("value", 0.01)
370 | g.Get("gain").Call("linearRampToValueAtTime", 0, currentTime+0.02)
371 |
372 | f := t.ctx.Call("createBiquadFilter")
373 | f.Call("connect", g)
374 | f.Set("type", "highpass")
375 | f.Get("frequency").Set("value", 5000)
376 |
377 | o := t.ctx.Call("createOscillator")
378 | o.Call("connect", f)
379 | var ended js.Func
380 | ended = js.FuncOf(func(t js.Value, args []js.Value) interface{} {
381 | g.Call("disconnect")
382 | ended.Release()
383 | return nil
384 | })
385 | o.Set("onended", ended)
386 | o.Call("start")
387 | o.Call("stop", currentTime+0.1)
388 | t.wn.Call("connect", f)
389 |
390 | }
391 |
392 | func (t *audioThing) createTune(freq float64) func() {
393 | return func() {
394 | currentTime := t.ctx.Get("currentTime").Float()
395 | g := t.ctx.Call("createGain")
396 | g.Call("connect", t.ctx.Get("destination"))
397 | g.Get("gain").Set("value", 0.03)
398 | g.Get("gain").Call("exponentialRampToValueAtTime", 0.0001, currentTime+2)
399 |
400 | o := t.ctx.Call("createOscillator")
401 | o.Set("type", "sine")
402 | o.Get("frequency").Call("linearRampToValueAtTime", freq, currentTime+0.02)
403 | o.Call("connect", g)
404 |
405 | var ended js.Func
406 | ended = js.FuncOf(func(t js.Value, args []js.Value) interface{} {
407 | g.Call("disconnect")
408 | ended.Release()
409 | return nil
410 | })
411 | o.Set("onended", ended)
412 | o.Call("start")
413 | o.Call("stop", currentTime+2)
414 | }
415 | }
416 |
--------------------------------------------------------------------------------
/bittune/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/bittune/main.wasm
--------------------------------------------------------------------------------
/bittune/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/bouncy/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/bouncy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - bouncy
4 |
5 |
6 |
7 |
8 |
35 |
60 |
61 |
62 |
65 |
66 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/bouncy/main.go:
--------------------------------------------------------------------------------
1 | //Wasming
2 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
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.Set("width", width)
29 | canvasEl.Set("height", height)
30 | ctx = canvasEl.Call("getContext", "2d")
31 |
32 | done := make(chan struct{}, 0)
33 |
34 | dt := DotThing{speed: 160, size: 6}
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 | // Event handler for count range
45 | countChangeEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
46 | evt := args[0]
47 | intVal, err := strconv.Atoi(evt.Get("target").Get("value").String())
48 | if err != nil {
49 | println("Invalid value", err)
50 | return nil
51 | }
52 | dt.SetNDots(intVal)
53 | return nil
54 | })
55 | defer countChangeEvt.Release()
56 |
57 | // Event handler for speed range
58 | speedInputEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
59 | evt := args[0]
60 | fval, err := strconv.ParseFloat(evt.Get("target").Get("value").String(), 64)
61 | if err != nil {
62 | println("invalid value", err)
63 | return nil
64 | }
65 | dt.speed = fval
66 | return nil
67 | })
68 | defer speedInputEvt.Release()
69 |
70 | // Event handler for size
71 | sizeChangeEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
72 | evt := args[0]
73 | intVal, err := strconv.Atoi(evt.Get("target").Get("value").String())
74 | if err != nil {
75 | println("invalid value", err)
76 | return nil
77 | }
78 | dt.size = intVal
79 | return nil
80 | })
81 | defer sizeChangeEvt.Release()
82 |
83 | // Event handler for lines toggle
84 | lineChangeEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
85 | evt := args[0]
86 | dt.lines = evt.Get("target").Get("checked").Bool()
87 | return nil
88 | })
89 | defer lineChangeEvt.Release()
90 |
91 | // Event handler for dashed toggle
92 | dashedChangeEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
93 | evt := args[0]
94 | dt.dashed = evt.Get("target").Get("checked").Bool()
95 | return nil
96 | })
97 | defer dashedChangeEvt.Release()
98 |
99 | doc.Call("addEventListener", "mousemove", mouseMoveEvt)
100 | doc.Call("getElementById", "count").Call("addEventListener", "change", countChangeEvt)
101 | doc.Call("getElementById", "speed").Call("addEventListener", "input", speedInputEvt)
102 | doc.Call("getElementById", "size").Call("addEventListener", "input", sizeChangeEvt)
103 | doc.Call("getElementById", "dashed").Call("addEventListener", "change", dashedChangeEvt)
104 | doc.Call("getElementById", "lines").Call("addEventListener", "change", lineChangeEvt)
105 |
106 | dt.SetNDots(100)
107 | dt.lines = false
108 | var renderFrame js.Func
109 | var tmark float64
110 | var markCount = 0
111 | var tdiffSum float64
112 |
113 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
114 | now := args[0].Float()
115 | tdiff := now - tmark
116 | tdiffSum += now - tmark
117 | markCount++
118 | if markCount > 10 {
119 | doc.Call("getElementById", "fps").Set("innerHTML", fmt.Sprintf("FPS: %.01f", 1000/(tdiffSum/float64(markCount))))
120 | tdiffSum, markCount = 0, 0
121 | }
122 | tmark = now
123 |
124 | // Pull window size to handle resize
125 | curBodyW := doc.Get("body").Get("clientWidth").Float()
126 | curBodyH := doc.Get("body").Get("clientHeight").Float()
127 | if curBodyW != width || curBodyH != height {
128 | width, height = curBodyW, curBodyH
129 | canvasEl.Set("width", width)
130 | canvasEl.Set("height", height)
131 | }
132 | dt.Update(tdiff / 1000)
133 |
134 | js.Global().Call("requestAnimationFrame", renderFrame)
135 | return nil
136 | })
137 | defer renderFrame.Release()
138 |
139 | // Start running
140 | js.Global().Call("requestAnimationFrame", renderFrame)
141 |
142 | <-done
143 |
144 | }
145 |
146 | // DotThing manager
147 | type DotThing struct {
148 | dots []*Dot
149 | dashed bool
150 | lines bool
151 | speed float64
152 | size int
153 | }
154 |
155 | // Update updates the dot positions and draws
156 | func (dt *DotThing) Update(dtTime float64) {
157 | if dt.dots == nil {
158 | return
159 | }
160 | ctx.Call("clearRect", 0, 0, width, height)
161 |
162 | // Update
163 | for i, dot := range dt.dots {
164 | dir := [2]float64{}
165 | // Bounce
166 | if dot.pos[0] < 0 {
167 | dot.pos[0] = 0
168 | dot.dir[0] *= -1
169 | }
170 | if dot.pos[0] > width {
171 | dot.pos[0] = width
172 | dot.dir[0] *= -1
173 | }
174 |
175 | if dot.pos[1] < 0 {
176 | dot.pos[1] = 0
177 | dot.dir[1] *= -1
178 | }
179 |
180 | if dot.pos[1] > height {
181 | dot.pos[1] = height
182 | dot.dir[1] *= -1
183 | }
184 | dir = dot.dir
185 |
186 | ctx.Set("globalAlpha", 0.5)
187 | ctx.Call("beginPath")
188 | ctx.Set("fillStyle", fmt.Sprintf("#%06x", dot.color))
189 | ctx.Set("strokeStyle", fmt.Sprintf("#%06x", dot.color))
190 | // Dashed array ref: https://github.com/golang/go/blob/release-branch.go1.11/src/syscall/js/js.go#L98
191 | ctx.Call("setLineDash", []interface{}{})
192 | if dt.dashed {
193 | ctx.Call("setLineDash", []interface{}{5, 10})
194 | }
195 | ctx.Set("lineWidth", dt.size)
196 | ctx.Call("arc", dot.pos[0], dot.pos[1], dt.size, 0, 2*math.Pi)
197 | ctx.Call("fill")
198 |
199 | mdx := mousePos[0] - dot.pos[0]
200 | mdy := mousePos[1] - dot.pos[1]
201 | d := math.Sqrt(mdx*mdx + mdy*mdy)
202 | if d < 200 {
203 | ctx.Set("globalAlpha", 1-d/200)
204 | ctx.Call("beginPath")
205 | ctx.Call("moveTo", dot.pos[0], dot.pos[1])
206 | ctx.Call("lineTo", mousePos[0], mousePos[1])
207 | ctx.Call("stroke")
208 | if d > 100 { // move towards mouse
209 | dir[0] = (mdx / d) * 2
210 | dir[1] = (mdy / d) * 2
211 | } else { // do not move
212 | dir[0] = 0
213 | dir[1] = 0
214 | }
215 | }
216 |
217 | if dt.lines {
218 | for _, dot2 := range dt.dots[i+1:] {
219 | mx := dot2.pos[0] - dot.pos[0]
220 | my := dot2.pos[1] - dot.pos[1]
221 | d := mx*mx + my*my
222 | if d < lineDistSq {
223 | ctx.Set("globalAlpha", 1-d/lineDistSq)
224 | ctx.Call("beginPath")
225 | ctx.Call("moveTo", dot.pos[0], dot.pos[1])
226 | ctx.Call("lineTo", dot2.pos[0], dot2.pos[1])
227 | ctx.Call("stroke")
228 | }
229 | }
230 | }
231 |
232 | dot.pos[0] += dir[0] * dt.speed * dtTime
233 | dot.pos[1] += dir[1] * dt.speed * dtTime
234 | }
235 | }
236 |
237 | // SetNDots reinitializes dots with n size
238 | func (dt *DotThing) SetNDots(n int) {
239 | dt.dots = make([]*Dot, n)
240 | for i := 0; i < n; i++ {
241 | dt.dots[i] = &Dot{
242 | pos: [2]float64{
243 | rand.Float64() * width,
244 | rand.Float64() * height,
245 | },
246 | dir: [2]float64{
247 | rand.NormFloat64(),
248 | rand.NormFloat64(),
249 | },
250 | color: uint32(rand.Intn(0xFFFFFF)),
251 | size: 10,
252 | }
253 | }
254 | }
255 |
256 | // Dot represents a dot ...
257 | type Dot struct {
258 | pos [2]float64
259 | dir [2]float64
260 | color uint32
261 | size float64
262 | }
263 |
--------------------------------------------------------------------------------
/bouncy/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/bouncy/main.wasm
--------------------------------------------------------------------------------
/bouncy/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/bumpy/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/bumpy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - bumpy
4 |
5 |
6 |
7 |
8 |
33 |
58 |
59 |
60 |
63 |
64 |
65 |
0
66 |
67 | Speed 1
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/bumpy/main.go:
--------------------------------------------------------------------------------
1 | // Click on canvas to start a polygon
2 | // - Max 8 vertices
3 | // - Only convex polygons
4 | // - Esc cancel polygon
5 |
6 | //Wasming
7 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
8 | package main
9 |
10 | import (
11 | "fmt"
12 | "math"
13 | "math/rand"
14 | "strconv"
15 | "syscall/js"
16 |
17 | // this box2d throws some unexpected panics
18 | "github.com/ByteArena/box2d"
19 | )
20 |
21 | var (
22 | width float64
23 | height float64
24 | ctx js.Value
25 | simSpeed float64 = 1
26 | worldScale float64 = 0.0125 // 1/8
27 | )
28 |
29 | func main() {
30 |
31 | // Init Canvas stuff
32 | doc := js.Global().Get("document")
33 | canvasEl := doc.Call("getElementById", "mycanvas")
34 | width = doc.Get("body").Get("clientWidth").Float()
35 | height = doc.Get("body").Get("clientHeight").Float()
36 | canvasEl.Call("setAttribute", "width", width)
37 | canvasEl.Call("setAttribute", "height", height)
38 | ctx = canvasEl.Call("getContext", "2d")
39 | ctx.Call("scale", 1/worldScale, 1/worldScale)
40 |
41 | done := make(chan struct{}, 0)
42 |
43 | world := box2d.MakeB2World(box2d.B2Vec2{X: 0, Y: 9.8})
44 | var verts []box2d.B2Vec2
45 |
46 | keyUpEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
47 | e := args[0]
48 | if e.Get("which").Int() == 27 {
49 | verts = nil
50 | }
51 | return nil
52 | })
53 | defer keyUpEvt.Release()
54 | mouseDownEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
55 | defer func() {
56 | // Recovering from possible box2d panic
57 | if r := recover(); r != nil {
58 | verts = nil
59 | }
60 | }()
61 |
62 | e := args[0]
63 | if e.Get("target") != canvasEl {
64 | return nil
65 | }
66 | mx := e.Get("clientX").Float() * worldScale
67 | my := e.Get("clientY").Float() * worldScale
68 | // Start shape
69 | if verts == nil {
70 | verts = []box2d.B2Vec2{box2d.B2Vec2{mx, my}}
71 | return nil
72 | }
73 | dx := mx - verts[0].X
74 | dy := my - verts[0].Y
75 | d := math.Sqrt(dx*dx + dy*dy)
76 | ///////
77 | // if clicked on the single spot we create a ball
78 | if len(verts) == 1 && d < 10*worldScale {
79 | obj1 := world.CreateBody(&box2d.B2BodyDef{
80 | Type: box2d.B2BodyType.B2_dynamicBody,
81 | Position: box2d.B2Vec2{X: mx, Y: my},
82 | Awake: true,
83 | Active: true,
84 | GravityScale: 1.0,
85 | })
86 | shape := box2d.NewB2CircleShape()
87 | shape.M_radius = (10 + rand.Float64()*10) * worldScale
88 | ft := obj1.CreateFixture(shape, 1)
89 | ft.M_friction = 0.3
90 | ft.M_restitution = 0.7
91 | verts = nil
92 | return nil
93 | }
94 | if len(verts) > 2 && d < 10*worldScale || len(verts) == 8 {
95 |
96 | // Seems box2d panics when we create a polygon counterclockwise most
97 | // likely due to normals and centroids calculations so basically we
98 | // recover from that panic and invert the polygon and try again
99 | var center *box2d.B2Vec2
100 | func() {
101 | defer func() { recover() }()
102 | lc := box2d.ComputeCentroid(verts, len(verts))
103 | center = &lc
104 | }()
105 | if center == nil {
106 | //vert inversion
107 | verts2 := make([]box2d.B2Vec2, len(verts))
108 | for i := range verts {
109 | verts2[len(verts)-1-i] = verts[i]
110 | }
111 | verts = verts2
112 | // Retry
113 | lc := box2d.ComputeCentroid(verts, len(verts))
114 | center = &lc
115 | }
116 |
117 | // translate -center
118 | for i := range verts {
119 | verts[i].X -= center.X
120 | verts[i].Y -= center.Y
121 | }
122 | shape := box2d.NewB2PolygonShape()
123 | shape.Set(verts, len(verts))
124 |
125 | obj := world.CreateBody(&box2d.B2BodyDef{
126 | Type: box2d.B2BodyType.B2_dynamicBody,
127 | Position: *center,
128 | Awake: true,
129 | Active: true,
130 | GravityScale: 1.0,
131 | })
132 | fixture := obj.CreateFixture(shape, 10)
133 | fixture.M_friction = 0.3
134 | verts = nil
135 | return nil
136 | }
137 | verts = append(verts, box2d.B2Vec2{mx, my})
138 | return nil
139 | })
140 | defer mouseDownEvt.Release()
141 |
142 | speedInputEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
143 | evt := args[0]
144 | fval, err := strconv.ParseFloat(evt.Get("target").Get("value").String(), 64)
145 | if err != nil {
146 | println("Invalid value", err)
147 | return nil
148 | }
149 | simSpeed = fval
150 | return nil
151 | })
152 | defer speedInputEvt.Release()
153 |
154 | doc.Call("addEventListener", "keyup", keyUpEvt)
155 | doc.Call("addEventListener", "mousedown", mouseDownEvt)
156 | doc.Call("getElementById", "speed").Call("addEventListener", "input", speedInputEvt)
157 |
158 | // Floor
159 | floor := world.CreateBody(&box2d.B2BodyDef{
160 | Type: box2d.B2BodyType.B2_kinematicBody,
161 | Position: box2d.B2Vec2{X: 0, Y: height*worldScale - 20*worldScale},
162 | Active: true,
163 | })
164 | floorShape := &box2d.B2PolygonShape{}
165 | floorShape.SetAsBox(width*worldScale, 20*worldScale)
166 | ft := floor.CreateFixture(floorShape, 1)
167 | ft.M_friction = 0.3
168 |
169 | // Some Random falling balls
170 | for i := 0; i < 10; i++ {
171 | obj1 := world.CreateBody(&box2d.B2BodyDef{
172 | Type: box2d.B2BodyType.B2_dynamicBody,
173 | Position: box2d.B2Vec2{X: rand.Float64() * width * worldScale, Y: rand.Float64() * height * worldScale},
174 | Awake: true,
175 | Active: true,
176 | GravityScale: 1.0,
177 | })
178 | shape := box2d.NewB2CircleShape()
179 | shape.M_radius = 10 * worldScale
180 | ft := obj1.CreateFixture(shape, 1)
181 | ft.M_friction = 0.3
182 | ft.M_restitution = 0.5 // bouncy
183 | }
184 |
185 | // Draw things
186 | var renderFrame js.Func
187 | var tmark float64
188 |
189 | // overall style
190 | ctx.Set("fillStyle", "rgba(100,150,100,0.4)")
191 | ctx.Set("strokeStyle", "rgba(100,150,100,1)")
192 | ctx.Set("lineWidth", 2*worldScale)
193 |
194 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
195 | now := args[0].Float()
196 | tdiff := now - tmark
197 | doc.Call("getElementById", "fps").Set("innerHTML", fmt.Sprintf("FPS: %.01f", 1000/tdiff))
198 | tmark = now
199 |
200 | // Pool window size to handle resize
201 | curBodyW := doc.Get("body").Get("clientWidth").Float()
202 | curBodyH := doc.Get("body").Get("clientHeight").Float()
203 | if curBodyW != width || curBodyH != height {
204 | width, height = curBodyW, curBodyH
205 | canvasEl.Set("width", width)
206 | canvasEl.Set("height", height)
207 | }
208 | world.Step(tdiff/1000*simSpeed, 60, 120)
209 |
210 | ctx.Call("clearRect", 0, 0, width*worldScale, height*worldScale)
211 |
212 | for curBody := world.GetBodyList(); curBody != nil; curBody = curBody.M_next {
213 | // Only one fixture for now
214 | ctx.Call("save")
215 | ft := curBody.M_fixtureList
216 | switch shape := ft.M_shape.(type) {
217 | case *box2d.B2PolygonShape: // Box
218 | // canvas translate
219 | ctx.Call("translate", curBody.M_xf.P.X, curBody.M_xf.P.Y)
220 | ctx.Call("rotate", curBody.M_xf.Q.GetAngle())
221 | ctx.Call("beginPath")
222 | ctx.Call("moveTo", shape.M_vertices[0].X, shape.M_vertices[0].Y)
223 | for _, v := range shape.M_vertices[1:shape.M_count] {
224 | ctx.Call("lineTo", v.X, v.Y)
225 | }
226 | ctx.Call("lineTo", shape.M_vertices[0].X, shape.M_vertices[0].Y)
227 | ctx.Call("fill")
228 | ctx.Call("stroke")
229 | case *box2d.B2CircleShape:
230 | ctx.Call("translate", curBody.M_xf.P.X, curBody.M_xf.P.Y)
231 | ctx.Call("rotate", curBody.M_xf.Q.GetAngle())
232 | ctx.Call("beginPath")
233 | ctx.Call("arc", 0, 0, shape.M_radius, 0, 2*math.Pi)
234 | ctx.Call("fill")
235 | ctx.Call("moveTo", 0, 0)
236 | ctx.Call("lineTo", 0, shape.M_radius)
237 | ctx.Call("stroke")
238 | }
239 | ctx.Call("restore")
240 |
241 | }
242 | // If we have a verts (mouse shape)
243 | if verts != nil {
244 | ctx.Call("save")
245 | ctx.Call("beginPath")
246 | ctx.Call("moveTo", verts[0].X, verts[0].Y)
247 | for _, v := range verts[1:] {
248 | ctx.Call("lineTo", v.X, v.Y)
249 | }
250 | ctx.Call("stroke")
251 |
252 | ctx.Set("lineWidth", 4*worldScale)
253 | for _, v := range verts { // Draw the clickPoints
254 | ctx.Call("beginPath")
255 | ctx.Call("arc", v.X, v.Y, 5*worldScale, 0, math.Pi*2)
256 | ctx.Call("stroke")
257 | }
258 | ctx.Call("restore")
259 | }
260 | js.Global().Call("requestAnimationFrame", renderFrame)
261 | return nil
262 | })
263 |
264 | // Start running
265 | js.Global().Call("requestAnimationFrame", renderFrame)
266 |
267 | <-done
268 |
269 | }
270 |
--------------------------------------------------------------------------------
/bumpy/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/bumpy/main.wasm
--------------------------------------------------------------------------------
/bumpy/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/stdiopt/gowasm-experiments
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/ByteArena/box2d v1.0.2
7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
8 | github.com/gorilla/websocket v1.4.0
9 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0
10 | github.com/lucasb-eyer/go-colorful v1.0.2
11 | github.com/pmezard/go-difflib v1.0.0 // indirect
12 | )
13 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/ByteArena/box2d v1.0.2 h1:f7f9KEQWhCs1n516DMLzi5w6u0MeeE78Mes4fWMcj9k=
2 | github.com/ByteArena/box2d v1.0.2/go.mod h1:LzEuxY9iCz+tskfWCY3o0ywYBRafDDugdSj+/YGI6sE=
3 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
4 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
5 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
6 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
9 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
10 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
11 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
12 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0 h1:2vp6ESimuT8pCuZHThVyV0hlfa9oPL06HnGCL9pbUgc=
13 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA=
14 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxDDkngVoI7/1MVhEl98Nm0KOk=
15 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
16 | github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
17 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
21 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
22 |
--------------------------------------------------------------------------------
/hexy/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/hexy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - what the hex
4 |
5 |
6 |
7 |
8 |
35 |
60 |
61 |
62 |
65 |
66 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/hexy/main.go:
--------------------------------------------------------------------------------
1 | // +build js
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "math"
8 | "math/rand"
9 | "syscall/js"
10 | )
11 |
12 | const (
13 | nPoints = 20
14 | hexE = 30
15 | speed = 2.5
16 | deg2rad = 0.017453292 // pi/180
17 | )
18 |
19 | var (
20 | hexH = math.Sqrt(3) * hexE
21 | hexD = float64(math.Round(3 * hexE))
22 | )
23 |
24 | func main() {
25 | ht := hexThing{}
26 | ht.SetNDots(nPoints)
27 | ht.start()
28 |
29 | }
30 |
31 | type hexThing struct {
32 | dots []*dot
33 |
34 | doc js.Value
35 | ctx js.Value
36 |
37 | backCtx []js.Value
38 | curCtx int
39 |
40 | width float64
41 | height float64
42 | fcount int
43 |
44 | // callbacks
45 | }
46 |
47 | func (ht *hexThing) start() {
48 | ht.doc = js.Global().Get("document")
49 | canvasEl := ht.doc.Call("getElementById", "mycanvas")
50 | ht.ctx = canvasEl.Call("getContext", "2d")
51 |
52 | // Create 2 backbuffers
53 | for i := 0; i < 2; i++ {
54 | cv := ht.doc.Call("createElement", "canvas")
55 | cv.Set("width", ht.width)
56 | cv.Set("height", ht.height)
57 | ctx := cv.Call("getContext", "2d")
58 | ht.backCtx = append(ht.backCtx, ctx)
59 | }
60 |
61 | var renderFrame js.Func
62 | var tmark float64
63 | var markCount = 0
64 | var tdiffSum float64
65 |
66 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
67 | // FPS thing
68 | now := args[0].Float()
69 | tdiff := now - tmark
70 | tdiffSum += now - tmark
71 | markCount++
72 | if markCount > 10 {
73 | ht.doc.Call("getElementById", "fps").Set("innerHTML", fmt.Sprintf("FPS: %.01f", 1000/(tdiffSum/float64(markCount))))
74 | tdiffSum, markCount = 0, 0
75 | }
76 | tmark = now
77 |
78 | // Canvas size handler
79 | curBodyW := ht.doc.Get("body").Get("clientWidth").Float() / 2
80 | curBodyH := ht.doc.Get("body").Get("clientHeight").Float() / 2
81 | if curBodyW != ht.width || curBodyH != ht.height {
82 | ht.width, ht.height = curBodyW, curBodyH
83 | canvasEl.Set("width", ht.width)
84 | canvasEl.Set("height", ht.height)
85 | // update back buffers too
86 | for _, cx := range ht.backCtx {
87 | cv := cx.Get("canvas")
88 | cv.Set("width", ht.width)
89 | cv.Set("height", ht.height)
90 | }
91 | }
92 |
93 | ht.Update(tdiff / 1000)
94 |
95 | js.Global().Call("requestAnimationFrame", renderFrame)
96 | return nil
97 | })
98 |
99 | done := make(chan struct{}, 0)
100 |
101 | js.Global().Call("requestAnimationFrame", renderFrame)
102 | <-done
103 | }
104 |
105 | func (ht *hexThing) Update(dtTime float64) {
106 | ht.fcount++
107 | prevCtx := ht.backCtx[ht.curCtx]
108 | ht.curCtx = (ht.curCtx + 1) % len(ht.backCtx)
109 | ctx := ht.backCtx[ht.curCtx]
110 |
111 | ctx.Call("clearRect", 0, 0, ht.width, ht.height)
112 | ctx.Set("globalAlpha", 1)
113 | filter := "hue-rotate(3deg) grayscale(1%)"
114 | if ht.fcount%120 == 0 {
115 | filter += " opacity(0.98)"
116 | }
117 | if ht.fcount%8 == 0 {
118 | filter += " blur(1px)"
119 | }
120 | ctx.Set("filter", filter)
121 | ctx.Call("drawImage", prevCtx.Get("canvas"), 0, 0)
122 | ctx.Set("filter", "")
123 |
124 | for _, d := range ht.dots {
125 | d.life -= 0.005
126 | if d.life < 0 { // Reset
127 | ht.dotReset(d)
128 | continue
129 | }
130 | d.r += speed
131 | if d.r >= hexE {
132 | sign := 1.0
133 | if rand.Float64()-0.5 < 0 {
134 | sign = -1.0
135 | }
136 | d.dir += 60 * sign
137 | d.r = 0
138 | }
139 |
140 | prevPos := d.pos
141 |
142 | d.x += math.Cos(d.dir*deg2rad) * speed
143 | d.y += math.Sin(d.dir*deg2rad) * speed
144 | // out of bounds
145 | if d.x < 0 || d.x > ht.width || d.y < 0 || d.y > ht.height {
146 | ht.dotReset(d)
147 | continue
148 | }
149 | ctx.Set("globalAlpha", d.life)
150 | ctx.Set("strokeStyle", "orange")
151 |
152 | ctx.Call("beginPath")
153 | ctx.Call("moveTo", prevPos.x, prevPos.y)
154 | ctx.Call("lineTo", d.x, d.y)
155 | ctx.Call("stroke")
156 | }
157 |
158 | ht.ctx.Call("clearRect", 0, 0, ht.width, ht.height)
159 | ht.ctx.Set("globalAlpha", 1)
160 | ht.ctx.Call("drawImage", ctx.Get("canvas"), 0, 0)
161 |
162 | }
163 |
164 | func (ht *hexThing) dotReset(d *dot) {
165 | fH := math.Floor(ht.width / hexH)
166 | fW := math.Floor(ht.height / hexD)
167 |
168 | *d = dot{
169 | pos: pos{
170 | x: math.Floor(rand.Float64()*fH) * hexH,
171 | y: math.Floor(rand.Float64()*fW) * hexD,
172 | },
173 | r: 0,
174 | dir: 90,
175 | life: 1 + rand.Float64(),
176 | }
177 |
178 | }
179 | func (ht *hexThing) SetNDots(n int) {
180 | ht.dots = make([]*dot, n)
181 | for i := 0; i < n; i++ {
182 | d := &dot{}
183 | ht.dotReset(d)
184 | ht.dots[i] = d
185 | }
186 | }
187 |
188 | type dot struct {
189 | pos
190 | r float64
191 | dir float64
192 | life float64
193 | }
194 |
195 | type pos struct {
196 | x float64
197 | y float64
198 | }
199 |
--------------------------------------------------------------------------------
/hexy/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/hexy/main.wasm
--------------------------------------------------------------------------------
/hexy/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/rainbow-mouse/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/rainbow-mouse/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - rainbow-mouse
4 |
5 |
6 |
7 |
8 |
25 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/rainbow-mouse/main.go:
--------------------------------------------------------------------------------
1 | //Wasming
2 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
3 | package main
4 |
5 | import (
6 | "math"
7 | "syscall/js"
8 |
9 | "github.com/lucasb-eyer/go-colorful"
10 | )
11 |
12 | var (
13 | mousePos [2]float64
14 | ctx js.Value
15 | )
16 |
17 | func main() {
18 |
19 | doc := js.Global().Get("document")
20 | canvasEl := js.Global().Get("document").Call("getElementById", "mycanvas")
21 |
22 | bodyW := doc.Get("body").Get("clientWidth").Float()
23 | bodyH := doc.Get("body").Get("clientHeight").Float()
24 | canvasEl.Set("width", bodyW)
25 | canvasEl.Set("height", bodyH)
26 | ctx = canvasEl.Call("getContext", "2d")
27 |
28 | done := make(chan struct{}, 0)
29 |
30 | colorRot := float64(0)
31 | curPos := []float64{100, 75}
32 |
33 | mouseMoveEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
34 | e := args[0]
35 | mousePos[0] = e.Get("clientX").Float()
36 | mousePos[1] = e.Get("clientY").Float()
37 | return nil
38 | })
39 | defer mouseMoveEvt.Release()
40 |
41 | doc.Call("addEventListener", "mousemove", mouseMoveEvt)
42 |
43 | var renderFrame js.Func
44 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
45 | // Handle window resizing
46 | curBodyW := doc.Get("body").Get("clientWidth").Float()
47 | curBodyH := doc.Get("body").Get("clientHeight").Float()
48 | if curBodyW != bodyW || curBodyH != bodyH {
49 | bodyW, bodyH = curBodyW, curBodyH
50 | canvasEl.Set("width", bodyW)
51 | canvasEl.Set("height", bodyH)
52 | }
53 | moveX := (mousePos[0] - curPos[0]) * 0.02
54 | moveY := (mousePos[1] - curPos[1]) * 0.02
55 |
56 | curPos[0] += moveX
57 | curPos[1] += moveY
58 |
59 | colorRot = float64(int(colorRot+1) % 360)
60 | ctx.Set("fillStyle", colorful.Hsv(colorRot, 1, 1).Hex())
61 | ctx.Call("beginPath")
62 | ctx.Call("arc", curPos[0], curPos[1], 50, 0, 2*math.Pi)
63 | ctx.Call("fill")
64 |
65 | js.Global().Call("requestAnimationFrame", renderFrame)
66 | return nil
67 | })
68 | defer renderFrame.Release()
69 |
70 | js.Global().Call("requestAnimationFrame", renderFrame)
71 |
72 | <-done
73 | }
74 |
--------------------------------------------------------------------------------
/rainbow-mouse/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/rainbow-mouse/main.wasm
--------------------------------------------------------------------------------
/rainbow-mouse/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/repulsion/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/repulsion/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - repulsion
4 |
5 |
6 |
7 |
8 |
34 |
59 |
60 |
61 |
64 |
65 |
66 |
0
67 |
68 | Speed 160
69 |
70 |
71 | Number of dots 100
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/repulsion/main.go:
--------------------------------------------------------------------------------
1 | //Wasming
2 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
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 |
113 | // DotThing manager
114 | type DotThing struct {
115 | dots []*Dot
116 | lines bool
117 | speed float64
118 | }
119 |
120 | // Update updates the dot positions and draws
121 | func (dt *DotThing) Update(dtTime float64) {
122 | if dt.dots == nil {
123 | return
124 | }
125 | ctx.Call("clearRect", 0, 0, width, height)
126 |
127 | // Update
128 | for i, dot := range dt.dots {
129 | if dot.pos[0] < dot.size {
130 | dot.pos[0] = dot.size
131 | dot.dir[0] *= -1
132 | }
133 | if dot.pos[0] > width-dot.size {
134 | dot.pos[0] = width - dot.size
135 | dot.dir[0] *= -1
136 | }
137 |
138 | if dot.pos[1] < dot.size {
139 | dot.pos[1] = dot.size
140 | dot.dir[1] *= -1
141 | }
142 |
143 | if dot.pos[1] > height-dot.size {
144 | dot.pos[1] = height - dot.size
145 | dot.dir[1] *= -1
146 | }
147 |
148 | mdx := mousePos[0] - dot.pos[0]
149 | mdy := mousePos[1] - dot.pos[1]
150 | d := math.Sqrt(mdx*mdx + mdy*mdy)
151 | if d < 200 {
152 | dInv := 1 - d/200
153 | dot.dir[0] += (-mdx / d) * dInv * 8
154 | dot.dir[1] += (-mdy / d) * dInv * 8
155 | }
156 | for j, dot2 := range dt.dots {
157 | if i == j {
158 | continue
159 | }
160 | mx := dot2.pos[0] - dot.pos[0]
161 | my := dot2.pos[1] - dot.pos[1]
162 | d := math.Sqrt(mx*mx + my*my)
163 | if d < 100 {
164 | dInv := 1 - d/100
165 | dot.dir[0] += (-mx / d) * dInv
166 | dot.dir[1] += (-my / d) * dInv
167 | }
168 | }
169 | dot.dir[0] *= 0.1 //friction
170 | dot.dir[1] *= 0.1 //friction
171 |
172 | dot.pos[0] += dot.dir[0] * dt.speed * dtTime * 10
173 | dot.pos[1] += dot.dir[1] * dt.speed * dtTime * 10
174 |
175 | ctx.Set("globalAlpha", 0.5)
176 | ctx.Call("beginPath")
177 | ctx.Set("fillStyle", fmt.Sprintf("#%06x", dot.color))
178 | ctx.Set("strokeStyle", fmt.Sprintf("#%06x", dot.color))
179 | ctx.Set("lineWidth", dot.size)
180 | ctx.Call("arc", dot.pos[0], dot.pos[1], dot.size, 0, 2*math.Pi)
181 | ctx.Call("fill")
182 |
183 | }
184 | }
185 |
186 | // SetNDots reinitializes dots with n size
187 | func (dt *DotThing) SetNDots(n int) {
188 | dt.dots = make([]*Dot, n)
189 | for i := 0; i < n; i++ {
190 | dt.dots[i] = &Dot{
191 | pos: [2]float64{
192 | rand.Float64() * width,
193 | rand.Float64() * height,
194 | },
195 | dir: [2]float64{
196 | rand.NormFloat64(),
197 | rand.NormFloat64(),
198 | },
199 | color: uint32(rand.Intn(0xFFFFFF)),
200 | size: 10,
201 | }
202 | }
203 | }
204 |
205 | // Dot represents a dot ...
206 | type Dot struct {
207 | pos [2]float64
208 | dir [2]float64
209 | color uint32
210 | size float64
211 | }
212 |
--------------------------------------------------------------------------------
/repulsion/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/repulsion/main.wasm
--------------------------------------------------------------------------------
/serve.go:
--------------------------------------------------------------------------------
1 | // Serve static files from current working directory
2 | // it will start at port 8080 if port is being used it will try next one
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "net"
9 | "net/http"
10 | "os"
11 | )
12 |
13 | func main() {
14 | port := 8080
15 | for {
16 | addr := fmt.Sprintf(":%d", port)
17 | listener, err := net.Listen("tcp", addr)
18 | if err != nil {
19 | fmt.Fprintln(os.Stderr, "err opening port", err)
20 | port++
21 | continue
22 | }
23 | fmt.Printf("Listening at %s\n", addr)
24 | log.Fatal(http.Serve(listener, logger(http.FileServer(http.Dir(".")))))
25 | }
26 | }
27 |
28 | func logger(next http.Handler) http.HandlerFunc {
29 | return func(w http.ResponseWriter, r *http.Request) {
30 | fmt.Println(r.Method, r.URL.Path)
31 | next.ServeHTTP(w, r)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/splashy/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Only works with go above 1.11beta1 current master (07-jun-2018)
3 | GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 |
--------------------------------------------------------------------------------
/splashy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | go webassembly - splashy
4 |
5 |
6 |
7 |
8 |
33 |
58 |
59 |
60 |
63 |
64 |
65 |
0
66 |
67 | Speed 1
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/splashy/main.go:
--------------------------------------------------------------------------------
1 | // Drag mouse on canvas
2 | //Wasming
3 | // compile: GOOS=js GOARCH=wasm go build -o main.wasm ./main.go
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "log"
9 | "math/rand"
10 | "strconv"
11 |
12 | "syscall/js"
13 | // this box2d throws some unexpected panics
14 | "github.com/ByteArena/box2d"
15 |
16 | colorful "github.com/lucasb-eyer/go-colorful"
17 | )
18 |
19 | var (
20 | width int
21 | height int
22 | ctx js.Value
23 | simSpeed = 1.0
24 | worldScale = 0.0125
25 | resDiv = 8
26 | maxBodies = 120
27 | )
28 |
29 | func main() {
30 |
31 | // Init Canvas stuff
32 | doc := js.Global().Get("document")
33 | canvasEl := doc.Call("getElementById", "mycanvas")
34 | width = doc.Get("body").Get("clientWidth").Int()
35 | height = doc.Get("body").Get("clientHeight").Int()
36 | canvasEl.Set("width", width)
37 | canvasEl.Set("height", height)
38 |
39 | gl := canvasEl.Call("getContext", "webgl")
40 | if gl == js.Undefined() {
41 | gl = canvasEl.Call("getContext", "experimental-webgl")
42 | }
43 | // once again
44 | if gl == js.Undefined() {
45 | js.Global().Call("alert", "browser might not support webgl")
46 | return
47 | }
48 |
49 | done := make(chan struct{}, 0)
50 |
51 | thing := Thing{}
52 | mouseDown := false
53 |
54 | mouseDownEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
55 | mouseDown = true
56 | evt := args[0]
57 | if evt.Get("target") != canvasEl {
58 | return nil
59 | }
60 | mx := evt.Get("clientX").Float() * worldScale
61 | my := evt.Get("clientY").Float() * worldScale
62 | thing.AddCircle(mx, my)
63 | return nil
64 | })
65 | defer mouseDownEvt.Release()
66 |
67 | mouseUpEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
68 | mouseDown = false
69 | return nil
70 | })
71 | defer mouseUpEvt.Release()
72 |
73 | mouseMoveEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
74 | if !mouseDown {
75 | return nil
76 | }
77 | evt := args[0]
78 | if evt.Get("target") != canvasEl {
79 | return nil
80 | }
81 | mx := evt.Get("clientX").Float() * worldScale
82 | my := evt.Get("clientY").Float() * worldScale
83 | thing.AddCircle(mx, my)
84 | return nil
85 | })
86 | defer mouseMoveEvt.Release()
87 |
88 | speedInputEvt := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
89 | evt := args[0]
90 | fval, err := strconv.ParseFloat(evt.Get("target").Get("value").String(), 64)
91 | if err != nil {
92 | println("Invalid value", err)
93 | return nil
94 | }
95 | simSpeed = fval
96 | return nil
97 | })
98 | defer speedInputEvt.Release()
99 | // Events
100 | doc.Call("addEventListener", "mousedown", mouseDownEvt)
101 | doc.Call("addEventListener", "mouseup", mouseUpEvt)
102 | doc.Call("addEventListener", "mousemove", mouseMoveEvt)
103 | doc.Call("getElementById", "speed").Call("addEventListener", "input", speedInputEvt)
104 |
105 | err := thing.Init(gl)
106 | if err != nil {
107 | println("Err Initializing thing:", err)
108 | return
109 | }
110 |
111 | // Draw things
112 | var renderFrame js.Func
113 | var tmark float64
114 | var markCount = 0
115 | var tdiffSum float64
116 |
117 | renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
118 | now := args[0].Float()
119 | tdiff := now - tmark
120 | tdiffSum += tdiff
121 | markCount++
122 | if markCount > 10 {
123 | doc.Call("getElementById", "fps").Set("innerHTML", fmt.Sprintf("FPS: %.01f", 1000/(tdiffSum/float64(markCount))))
124 | tdiffSum, markCount = 0, 0
125 | }
126 | tmark = now
127 | // --
128 | thing.Render(gl, tdiff/1000)
129 |
130 | js.Global().Call("requestAnimationFrame", renderFrame)
131 | return nil
132 | })
133 | defer renderFrame.Release()
134 |
135 | // Start running
136 | js.Global().Call("requestAnimationFrame", renderFrame)
137 |
138 | <-done
139 |
140 | }
141 |
142 | type Thing struct {
143 | // dot shaders
144 | prog js.Value
145 | aPosition js.Value
146 | uFragColor js.Value
147 | uResolution js.Value
148 |
149 | dotBuf js.Value
150 | qBlur *QuadFX
151 | qThreshold *QuadFX
152 |
153 | rtTex [2]js.Value // render target Texture
154 | rt [2]js.Value // framebuffer(render target)
155 |
156 | world box2d.B2World
157 | }
158 |
159 | func (t *Thing) Init(gl js.Value) error {
160 | // Drawing program
161 | var err error
162 | t.prog, err = programFromSrc(gl, dotVertShader, dotFragShader)
163 | if err != nil {
164 | return err
165 | }
166 | t.aPosition = gl.Call("getAttribLocation", t.prog, "a_position")
167 | t.uFragColor = gl.Call("getUniformLocation", t.prog, "uFragColor")
168 | t.uResolution = gl.Call("getUniformLocation", t.prog, "uResolution")
169 |
170 | t.dotBuf = gl.Call("createBuffer", gl.Get("ARRAY_BUFFER"))
171 | //renderer targets
172 | for i := 0; i < 2; i++ {
173 | t.rtTex[i] = createTexture(gl, width/resDiv, height/resDiv)
174 | t.rt[i] = createFB(gl, t.rtTex[i])
175 | }
176 |
177 | t.qBlur = &QuadFX{}
178 | err = t.qBlur.Init(gl, blurShader)
179 | if err != nil {
180 | log.Fatal("Error:", err)
181 | }
182 | t.qThreshold = &QuadFX{}
183 | err = t.qThreshold.Init(gl, thresholdShader)
184 | if err != nil {
185 | log.Fatal("Error:", err)
186 | }
187 |
188 | //////////////////////////
189 | // Physics
190 | // ///////////
191 | t.world = box2d.MakeB2World(box2d.B2Vec2{X: 0, Y: 9.8})
192 | floor := t.world.CreateBody(&box2d.B2BodyDef{
193 | Type: box2d.B2BodyType.B2_kinematicBody,
194 | Position: box2d.B2Vec2{X: 0, Y: float64(height+10) * worldScale},
195 | Active: true,
196 | })
197 | floorShape := &box2d.B2PolygonShape{}
198 | floorShape.SetAsBox(float64(width)*worldScale, 20*worldScale)
199 | ft := floor.CreateFixture(floorShape, 1)
200 | ft.M_friction = 0.3
201 |
202 | // Walls
203 | wallShape := &box2d.B2PolygonShape{}
204 | wallShape.SetAsBox(20*worldScale, float64(height)*worldScale)
205 |
206 | wallL := t.world.CreateBody(&box2d.B2BodyDef{
207 | Type: box2d.B2BodyType.B2_kinematicBody,
208 | Position: box2d.B2Vec2{X: 0, Y: 0},
209 | Active: true,
210 | })
211 | wlf := wallL.CreateFixture(wallShape, 1)
212 | wlf.M_friction = 0.3
213 |
214 | wallR := t.world.CreateBody(&box2d.B2BodyDef{
215 | Type: box2d.B2BodyType.B2_kinematicBody,
216 | Position: box2d.B2Vec2{X: float64(width) * worldScale, Y: 0},
217 | Active: true,
218 | })
219 | wrt := wallR.CreateFixture(wallShape, 1)
220 | wrt.M_friction = 0.3
221 |
222 | for i := 0; i < 10; i++ {
223 | t.AddCircle(rand.Float64()*float64(width)*worldScale, rand.Float64()*float64(height)*worldScale)
224 | }
225 |
226 | return nil
227 | }
228 |
229 | func (t *Thing) Render(gl js.Value, dtTime float64) {
230 |
231 | texWidth := width / resDiv
232 | texHeight := height / resDiv
233 | t.world.Step(dtTime*simSpeed, 3, 3)
234 |
235 | gl.Call("bindFramebuffer", gl.Get("FRAMEBUFFER"), t.rt[0])
236 | gl.Call("viewport", 0, 0, texWidth, texHeight) //texSize
237 | gl.Call("clearColor", 0, 0, 0, 0)
238 | gl.Call("clear", gl.Get("COLOR_BUFFER_BIT"))
239 |
240 | // DotRenderer
241 | gl.Call("useProgram", t.prog)
242 |
243 | count := 0
244 | for curBody := t.world.GetBodyList(); curBody != nil; curBody = curBody.M_next {
245 | ft := curBody.M_fixtureList
246 | if _, ok := ft.M_shape.(*box2d.B2CircleShape); !ok {
247 | continue
248 | }
249 | x := float32(curBody.M_xf.P.X / (float64(width) * worldScale)) /* 0-1 */
250 | y := float32(curBody.M_xf.P.Y / (float64(height) * worldScale)) /*0-1*/
251 |
252 | col := colorful.Hsv(float64(360*count/maxBodies), 1, 1)
253 | gl.Call("vertexAttrib2f", t.aPosition, x, y)
254 | gl.Call("uniform4f", t.uFragColor, col.R, col.G, col.B, 1.0)
255 | gl.Call("drawArrays", gl.Get("POINTS"), 0, 1)
256 |
257 | count++
258 | // Stop processing
259 | if count > maxBodies {
260 | break
261 | }
262 | }
263 |
264 | /// FX Blurx4 TODO: better blur
265 | for i := 0; i < 4; i++ {
266 | gl.Call("bindFramebuffer", gl.Get("FRAMEBUFFER"), t.rt[1])
267 | gl.Call("viewport", 0, 0, texWidth, texHeight)
268 | gl.Call("bindTexture", gl.Get("TEXTURE_2D"), t.rtTex[0])
269 | t.qBlur.Render(gl)
270 |
271 | gl.Call("bindFramebuffer", gl.Get("FRAMEBUFFER"), t.rt[0])
272 | gl.Call("viewport", 0, 0, texWidth, texHeight)
273 | gl.Call("bindTexture", gl.Get("TEXTURE_2D"), t.rtTex[1])
274 | t.qBlur.Render(gl)
275 | }
276 |
277 | /// FX Threshold to Screen
278 | gl.Call("bindFramebuffer", gl.Get("FRAMEBUFFER"), nil)
279 | gl.Call("viewport", 0, 0, width, height)
280 | gl.Call("bindTexture", gl.Get("TEXTURE_2D"), t.rtTex[0])
281 | t.qThreshold.Render(gl)
282 |
283 | }
284 |
285 | func (t *Thing) AddCircle(mx, my float64) {
286 | if t.world.GetBodyCount() > maxBodies {
287 | // Check for the last on list and delete backwards:o
288 | var b *box2d.B2Body
289 | // theres is no M_last but we could cache it somewhere
290 | for b = t.world.GetBodyList(); b.M_next != nil; b = b.M_next {
291 | }
292 | // Search backwards for a circle (ignoring the walls/floors)
293 | for ; b != nil; b = b.M_prev {
294 | if _, ok := b.M_fixtureList.M_shape.(*box2d.B2CircleShape); ok {
295 | t.world.DestroyBody(b) // Destroy first found body
296 | break
297 | }
298 | }
299 | }
300 | obj1 := t.world.CreateBody(&box2d.B2BodyDef{
301 | Type: box2d.B2BodyType.B2_dynamicBody,
302 | Position: box2d.B2Vec2{X: mx, Y: my},
303 | Awake: true,
304 | Active: true,
305 | GravityScale: 1.0,
306 | })
307 | shape := box2d.NewB2CircleShape()
308 | shape.M_radius = 10 * worldScale
309 | ft := obj1.CreateFixture(shape, 1)
310 | ft.M_friction = 0.2
311 | ft.M_restitution = 0.6
312 | }
313 |
314 | //// SHADERS & Utils
315 | const dotVertShader = `
316 | attribute vec4 a_position;
317 | void main () {
318 | vec4 lpos= vec4(a_position.xy*2.0-1.0, 0, 1);
319 | lpos.y = -lpos.y;
320 | gl_Position = lpos;
321 | gl_PointSize = 22.0/4.0;
322 | }
323 | `
324 | const dotFragShader = `
325 | precision mediump float;
326 | uniform vec4 uFragColor;
327 | void main () {
328 | vec2 pt = gl_PointCoord - vec2(0.5);
329 | if(pt.x*pt.x+pt.y*pt.y > 0.25)
330 | discard;
331 | gl_FragColor = uFragColor;
332 | }
333 | `
334 |
335 | const blurShader = `
336 | precision mediump float;
337 | uniform sampler2D u_image;
338 | uniform vec2 u_textureSize;
339 | varying vec2 v_texCoord;
340 | void main() {
341 | vec2 onePixel = vec2(1,1) / u_textureSize;
342 | vec4 colorSum =
343 | texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) +
344 | texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) +
345 | texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) +
346 | texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) +
347 | texture2D(u_image, v_texCoord + onePixel * vec2( 0, 0)) +
348 | texture2D(u_image, v_texCoord + onePixel * vec2( 1, 0)) +
349 | texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) +
350 | texture2D(u_image, v_texCoord + onePixel * vec2( 0, 1)) +
351 | texture2D(u_image, v_texCoord + onePixel * vec2( 1, 1));
352 | gl_FragColor = colorSum / 9.0;
353 | }
354 | `
355 |
356 | const thresholdShader = `
357 | precision mediump float;
358 | uniform sampler2D u_image;
359 | uniform vec2 u_textureSize;
360 | varying vec2 v_texCoord;
361 | void main() {
362 | float a;
363 | vec2 onePixel = vec2(1,1) / u_textureSize;
364 | vec4 col = texture2D(u_image,v_texCoord);
365 | if (col.a < 0.4) discard;
366 | if (col.a < 0.8 && col.a > 0.72) {
367 | a = texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)).a;
368 | if (a < col.a ) {
369 | col += 0.4;
370 | }
371 | }
372 | gl_FragColor = vec4(col.rgb,1.0);
373 | }
374 | `
375 |
376 | const vertQuad = `
377 | attribute vec2 a_position;
378 | attribute vec2 a_texCoord;
379 | varying vec2 v_texCoord;
380 | void main() {
381 | gl_Position = vec4((a_position * 2.0 - 1.0), 0, 1);
382 | v_texCoord = a_texCoord;
383 | }
384 | `
385 |
386 | type QuadFX struct {
387 | prog js.Value
388 | aPosition js.Value
389 | aTexCoord js.Value
390 | uTextureSize js.Value
391 |
392 | quadBuf js.Value
393 |
394 | vertexData js.Value
395 | }
396 |
397 | func (q *QuadFX) Init(gl js.Value, frag string) error {
398 | var err error
399 | q.prog, err = programFromSrc(gl, vertQuad, frag)
400 | if err != nil {
401 | return err
402 | }
403 | vertexData := []float32{
404 | 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
405 | 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
406 | }
407 | // go1.13 removed TypedArrayOf https://github.com/golang/go/issues/31980
408 | q.vertexData = js.Global().Get("Float32Array").New(12)
409 | for i, v := range vertexData {
410 | q.vertexData.SetIndex(i, v)
411 | }
412 |
413 | q.aPosition = gl.Call("getAttribLocation", q.prog, "a_position")
414 | q.aTexCoord = gl.Call("getAttribLocation", q.prog, "a_texCoord")
415 | q.uTextureSize = gl.Call("getUniformLocation", q.prog, "u_textureSize")
416 |
417 | q.quadBuf = gl.Call("createBuffer")
418 | // texCoord/posCoord
419 | gl.Call("bindBuffer", gl.Get("ARRAY_BUFFER"), q.quadBuf)
420 | gl.Call("bufferData", gl.Get("ARRAY_BUFFER"), q.vertexData, gl.Get("STATIC_DRAW"))
421 | return nil
422 |
423 | }
424 | func (q *QuadFX) Render(gl js.Value) {
425 | gl.Call("useProgram", q.prog)
426 | // Vertex
427 | gl.Call("bindBuffer", gl.Get("ARRAY_BUFFER"), q.quadBuf)
428 |
429 | gl.Call("enableVertexAttribArray", q.aPosition)
430 | gl.Call("vertexAttribPointer", q.aPosition, 2, gl.Get("FLOAT"), false, 0, 0)
431 | gl.Call("enableVertexAttribArray", q.aTexCoord) // sabe buf
432 | gl.Call("vertexAttribPointer", q.aTexCoord, 2, gl.Get("FLOAT"), false, 0, 0)
433 |
434 | gl.Call("uniform2f", q.uTextureSize, width/resDiv, height/resDiv)
435 |
436 | gl.Call("drawArrays", gl.Get("TRIANGLES"), 0 /*offset*/, 6 /*count*/)
437 | gl.Call("disableVertexAttribArray", q.aPosition)
438 | gl.Call("disableVertexAttribArray", q.aTexCoord)
439 |
440 | }
441 |
442 | func (q *QuadFX) Release() {
443 | // TODO: gl release
444 | }
445 |
446 | // Helper funcs
447 |
448 | // Render to framebuffer first, then framebuffer to screen
449 | func compileShader(gl, shaderType js.Value, shaderSrc string) (js.Value, error) {
450 | var shader = gl.Call("createShader", shaderType)
451 | gl.Call("shaderSource", shader, shaderSrc)
452 | gl.Call("compileShader", shader)
453 |
454 | if !gl.Call("getShaderParameter", shader, gl.Get("COMPILE_STATUS")).Bool() {
455 | return js.Undefined(), fmt.Errorf("could not compile shader: %v", gl.Call("getShaderInfoLog", shader).String())
456 | }
457 | return shader, nil
458 | }
459 |
460 | func linkProgram(gl, vertexShader, fragmentShader js.Value) (js.Value, error) {
461 | var program = gl.Call("createProgram")
462 | gl.Call("attachShader", program, vertexShader)
463 | gl.Call("attachShader", program, fragmentShader)
464 | gl.Call("linkProgram", program)
465 | if !gl.Call("getProgramParameter", program, gl.Get("LINK_STATUS")).Bool() {
466 | return js.Undefined(), fmt.Errorf("could not link program: %v", gl.Call("getProgramInfoLog", program).String())
467 | }
468 |
469 | return program, nil
470 | }
471 |
472 | func programFromSrc(gl js.Value, vertShaderSrc, fragShaderSrc string) (js.Value, error) {
473 | vertexShader, err := compileShader(gl, gl.Get("VERTEX_SHADER"), vertShaderSrc)
474 | if err != nil {
475 | return js.Undefined(), err
476 | }
477 | fragShader, err := compileShader(gl, gl.Get("FRAGMENT_SHADER"), fragShaderSrc)
478 | if err != nil {
479 | return js.Undefined(), err
480 | }
481 | prog, err := linkProgram(gl, vertexShader, fragShader)
482 | if err != nil {
483 | return js.Undefined(), err
484 | }
485 | return prog, nil
486 | }
487 |
488 | func createTexture(gl js.Value, width, height int) js.Value {
489 | tex := gl.Call("createTexture")
490 | gl.Call("bindTexture", gl.Get("TEXTURE_2D"), tex)
491 | // define size and format of level 0
492 | gl.Call("texImage2D",
493 | gl.Get("TEXTURE_2D"),
494 | 0,
495 | gl.Get("RGBA"),
496 | width, height,
497 | 0,
498 | gl.Get("RGBA"),
499 | gl.Get("UNSIGNED_BYTE"),
500 | js.Null(),
501 | )
502 |
503 | gl.Call("texParameteri", gl.Get("TEXTURE_2D"), gl.Get("TEXTURE_MIN_FILTER"), gl.Get("LINEAR"))
504 | gl.Call("texParameteri", gl.Get("TEXTURE_2D"), gl.Get("TEXTURE_MAG_FILTER"), gl.Get("LINEAR"))
505 | gl.Call("texParameteri", gl.Get("TEXTURE_2D"), gl.Get("TEXTURE_WRAP_S"), gl.Get("CLAMP_TO_EDGE"))
506 | gl.Call("texParameteri", gl.Get("TEXTURE_2D"), gl.Get("TEXTURE_WRAP_T"), gl.Get("CLAMP_TO_EDGE"))
507 |
508 | return tex
509 | }
510 |
511 | // Create framebuffer binded to texture
512 | func createFB(gl, tex js.Value) js.Value {
513 | fb := gl.Call("createFramebuffer")
514 | gl.Call("bindFramebuffer", gl.Get("FRAMEBUFFER"), fb)
515 | gl.Call("framebufferTexture2D", gl.Get("FRAMEBUFFER"), gl.Get("COLOR_ATTACHMENT0"), gl.Get("TEXTURE_2D"), tex, 0)
516 | return fb
517 | }
518 |
--------------------------------------------------------------------------------
/splashy/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdiopt/gowasm-experiments/fe96c7b6247a5fa76c1008732a701313753647e5/splashy/main.wasm
--------------------------------------------------------------------------------
/splashy/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------