├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .markdownlint.yaml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── Taskfile.yml ├── audio ├── analyser_node.go ├── audio_context.go ├── audio_node.go ├── audio_param.go ├── biquad_filter.go ├── destination_node.go ├── gain_node.go ├── media_stream.go ├── media_stream_source_node.go ├── oscillator.go └── value.go ├── canvas ├── context.go ├── context2d.go ├── line.go ├── rectangle.go ├── shadow.go └── text.go ├── css └── style_declaration.go ├── examples ├── README.md ├── ball │ └── main.go ├── bootstrap │ └── main.go ├── breakout │ ├── ball.go │ ├── ball_test.go │ ├── brick.go │ ├── bricks.go │ ├── game.go │ ├── main.go │ ├── main_test.go │ ├── platform.go │ ├── settings.go │ ├── shapes.go │ ├── state.go │ ├── test.sh │ ├── text_block.go │ └── vector.go ├── build.sh ├── build_all.py ├── draw │ └── main.go ├── events │ └── main.go ├── frontend │ ├── index.html │ ├── loader.js │ └── style.css ├── hello │ └── main.go ├── http_request │ └── main.go ├── index.html.j2 ├── index.yml ├── oscilloscope │ └── main.go ├── pacman │ └── main.go ├── piano │ ├── key.go │ ├── keyboard.go │ ├── main.go │ └── sound.go ├── run.sh ├── server │ └── main.go ├── styling │ └── main.go ├── templates │ └── main.go └── triangle │ └── main.go ├── generate_refs.py ├── go.mod ├── go.sum ├── netlify.toml ├── refs.md ├── requirements.txt ├── tools.go └── web ├── canvas.go ├── console.go ├── console_test.go ├── document.go ├── document_test.go ├── element.go ├── element_test.go ├── embed.go ├── event.go ├── event_target.go ├── html_element.go ├── http_request.go ├── http_request_test.go ├── media_devices.go ├── navigator.go ├── node.go ├── promise.go ├── screen.go ├── value.go ├── window.go └── window_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # EditorConfig helps developers define and maintain consistent 3 | # coding styles between different editors and IDEs 4 | # https://editorconfig.org 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.{md,rst,txt}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.{html,html.j2,yml,js}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | go-version: 21 | - "1.18" 22 | - "1.19" 23 | - "1.20" 24 | - "1.21" 25 | steps: 26 | - uses: actions/setup-go@v3 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | - uses: actions/checkout@v3 30 | - uses: arduino/setup-task@v1 31 | - run: task test 32 | 33 | markdownlint-cli: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: nosborn/github-action-markdown-cli@v3.2.0 38 | with: 39 | files: . 40 | config_file: .markdownlint.yaml 41 | dot: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /examples/build/ 2 | /examples/server.bin 3 | /build/ 4 | /public/ 5 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | default: true # enable all by default 3 | MD007: # unordered list indentation 4 | indent: 2 5 | MD013: false # do not validate line length 6 | MD014: false # allow $ before command output 7 | MD029: # ordered list prefix 8 | style: "one" 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // set os and arch to be able to use `syscall/js` and run tests 3 | "go.toolsEnvVars": { 4 | "GOARCH": "wasm", 5 | "GOOS": "js", 6 | }, 7 | "go.testEnvVars": { 8 | "GOARCH": "wasm", 9 | "GOOS": "js", 10 | }, 11 | 12 | // run tests in a headless chromium 13 | "go.testFlags": ["-exec=wasmbrowsertest"], 14 | 15 | // disable coverage since it requires to write output in a file 16 | // but js+wasm compilation forbids file system access 17 | "go.coverOnSave": false, 18 | "go.coverOnSingleTest": false, 19 | "go.coverOnTestPackage": false, 20 | "go.coverOnSingleTestFile": false, 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2020 Gram 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GWeb: golang + js + wasm 2 | 3 | **gweb** -- strictly typed [WebAPI](https://en.wikipedia.org/wiki/Web_API) library on top of [syscall/js](https://golang.org/pkg/syscall/js/). Like [flow](https://github.com/facebook/flow) or [TypeScript](https://www.typescriptlang.org/) but for Go. You need it if you want to interact with browser from [wasm-compiled](https://github.com/golang/go/wiki/WebAssembly) Go program. 4 | 5 | + Examples: [gweb.orsinium.dev](https://gweb.orsinium.dev/) 6 | + Mapping of JS Web API to GWeb functions: [refs.md](./refs.md) 7 | 8 | ## Features 9 | 10 | + **Strictly typed**. It's a wrapper around `syscall/js` that helps you to avoid runtime errors (you'll get them a lot with raw `syscall/js`). 11 | + **Backward compatible**. Almost every type is a wrapper around `js.Value`. So if something missed, you can always fall back to the classic `syscall/js` calls. 12 | + **Hand crafted**. It's hard to make a usable autogeneration of WebAPI since Go is a strictly typed language without union types. So we carefully translated everything while applying Go best practices. 13 | + **Cleaned up**. The library provides only useful methods and attributes from WebAPI. No obsolete and deprecated methods, no experimental APIs that are only supported by a few engines. Only what we really need right now. 14 | + **Almost the same API as in JS**. If you have experience with [vanilla JS](https://stackoverflow.com/a/20435744), you have almost learnt everything about the libray. 15 | + **But better**. WebAPI has a long history of incremental changes and spaces for unimplemented dreams. However, we can see the full picture to provide a better experience and more namespaces. 16 | + **Documented**. Every method is documented to save your time and reduce googling. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | GOOS=js GOARCH=wasm go get github.com/life4/gweb 22 | ``` 23 | 24 | If you're using VSCode, it's recommend to create a `.vscode/settings.json` file in your project with the following content: 25 | 26 | ```json 27 | { 28 | "go.toolsEnvVars": { 29 | "GOARCH": "wasm", 30 | "GOOS": "js", 31 | }, 32 | "go.testEnvVars": { 33 | "GOARCH": "wasm", 34 | "GOOS": "js", 35 | }, 36 | } 37 | ``` 38 | 39 | ## Error handling 40 | 41 | In the beautiful JS world anything at any time can be `null` or `undefined`. Check it when you're not sure: 42 | 43 | ```go 44 | doc := web.GetWindow().Document() 45 | el := doc.Element("some-element-id") 46 | if el.Type() == js.TypeNull { 47 | // handle error 48 | } 49 | ``` 50 | 51 | ## Missed API 52 | 53 | If something is missed, use `syscall/js`-like methods (`Get`, `Set`, `Call` etc): 54 | 55 | ```go 56 | doc := web.GetWindow().Document() 57 | el := doc.Element("some-element-id") 58 | name = el.Get("name").String() 59 | ``` 60 | 61 | ## Packages 62 | 63 | GWeb is a collection of a few packages: 64 | 65 | + `web` ([docs](https://pkg.go.dev/github.com/life4/gweb/web?tab=doc)) -- window, manipulations with DOM. 66 | + `audio` ([docs](https://pkg.go.dev/github.com/life4/gweb/audio?tab=doc)) -- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). Use `web.GetWindow().AudioContext()` as an entry point. 67 | + `canvas` ([docs](https://pkg.go.dev/github.com/life4/gweb/canvas?tab=doc)) -- canvas-related objects. Use `web.GetWindow().Document().CreateCanvas()` to get started. 68 | + `css` ([docs](https://pkg.go.dev/github.com/life4/gweb/css?tab=doc)) -- manage styles for HTML elements. 69 | 70 | ## Contributing 71 | 72 | Contributions are welcome! GWeb is a Open-Source project and you can help to make it better. Some ideas what can be improved: 73 | 74 | + Every function and object should have short description based on [MDN Web API docs](https://developer.mozilla.org/en-US/docs/Web/API). Some descriptions are missed. 75 | + Also, every function that calls a Web API method should have a link in docs for that method. 76 | + Typos are very possible, don't be shy to fix it if you've spotted one. 77 | + More objects and methods? Of course! Our goal is to cover everything in WebAPI! Well, excluding deprecated things. See [Features](#features) section to get feeling what should be there. 78 | + Found a bug? Fix it! 79 | 80 | And even if you don't have spare time for making PRs, you still can help by talking to your friends and subscribers about GWeb. Thank you :heart: 81 | 82 | ## Similar projects 83 | 84 | + [webapi](https://github.com/gowebapi/webapi/) -- bindings that autogenerated from [WebIDL](https://heycam.github.io/webidl/). 85 | + [godom](https://github.com/siongui/godom) -- bindings on top of [gopherjs](github.com/gopherjs/gopherjs/). 86 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | version: '3' 3 | 4 | vars: 5 | GOPATH: 6 | sh: go env GOPATH 7 | 8 | tasks: 9 | install: 10 | cmds: 11 | - go mod download 12 | - go install github.com/agnivade/wasmbrowsertest 13 | 14 | test:library: 15 | deps: 16 | - install 17 | env: 18 | GOOS: js 19 | GOARCH: wasm 20 | cmds: 21 | - go test -exec={{.GOPATH}}/bin/wasmbrowsertest -buildvcs=false ./web/ 22 | 23 | test:examples: 24 | env: 25 | GOOS: js 26 | GOARCH: wasm 27 | cmds: 28 | - rm -rf /tmp/gweb-bin 29 | - mkdir -p /tmp/gweb-bin/ 30 | - go build -buildvcs=false -o /tmp/gweb-bin/ ./... 31 | 32 | test: 33 | desc: "run go test for the library and examples" 34 | cmds: 35 | - task: test:library 36 | - task: test:examples 37 | 38 | refs: 39 | cmds: 40 | - python3 generate_refs.py > refs.md 41 | -------------------------------------------------------------------------------- /audio/analyser_node.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "syscall/js" 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode 6 | type AnalyserNode struct { 7 | AudioNode 8 | } 9 | 10 | func (analyser AnalyserNode) FrequencyData() FrequencyDataBytes { 11 | size := analyser.FFTSize() 12 | return FrequencyDataBytes{ 13 | node: analyser.Value, 14 | container: js.Global().Get("Uint8Array").New(size), 15 | Size: size, 16 | Data: make([]byte, size), 17 | } 18 | } 19 | 20 | func (analyser AnalyserNode) TimeDomain() TimeDomainBytes { 21 | size := analyser.FFTSize() 22 | return TimeDomainBytes{ 23 | node: analyser.Value, 24 | container: js.Global().Get("Uint8Array").New(size), 25 | Size: size, 26 | Data: make([]byte, size), 27 | } 28 | } 29 | 30 | // FFTSize represents the window size in samples that is used 31 | // when performing a Fast Fourier Transform (FFT) to get frequency domain data. 32 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize 33 | func (analyser AnalyserNode) FFTSize() int { 34 | return analyser.Get("fftSize").Int() 35 | } 36 | 37 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/frequencyBinCount 38 | func (analyser AnalyserNode) FrequencyBinCount() int { 39 | return analyser.Get("frequencyBinCount").Int() 40 | } 41 | 42 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels 43 | func (analyser AnalyserNode) MinDecibels() int { 44 | return analyser.Get("minDecibels").Int() 45 | } 46 | 47 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/msxDecibels 48 | func (analyser AnalyserNode) MaxDecibels() int { 49 | return analyser.Get("maxDecibels").Int() 50 | } 51 | 52 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant 53 | func (analyser AnalyserNode) SmoothingTimeConstant() float64 { 54 | return analyser.Get("smoothingTimeConstant").Float() 55 | } 56 | 57 | // SETTERS 58 | 59 | // FFTSize represents the window size in samples that is used 60 | // when performing a Fast Fourier Transform (FFT) to get frequency domain data. 61 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize 62 | func (analyser AnalyserNode) SetFFTSize(value int) { 63 | analyser.Set("fftSize", value) 64 | } 65 | 66 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels 67 | func (analyser AnalyserNode) SetMinDecibels(value int) { 68 | analyser.Set("minDecibels", value) 69 | } 70 | 71 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels 72 | func (analyser AnalyserNode) SetMaxDecibels(value int) { 73 | analyser.Set("maxDecibels", value) 74 | } 75 | 76 | // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant 77 | func (analyser AnalyserNode) SetSmoothingTimeConstant(value float64) { 78 | analyser.Set("smoothingTimeConstant", value) 79 | } 80 | 81 | // SUBTYPES 82 | 83 | type TimeDomainBytes struct { 84 | node js.Value // AnalyserNode value to do method call 85 | container js.Value // where to read data in JS 86 | Size int // Size of the data array 87 | Data []byte // where to copy data from JS into Go 88 | } 89 | 90 | // Update reads the current waveform or time-domain into `Data` attribute. 91 | func (domain *TimeDomainBytes) Update() { 92 | domain.node.Call("getByteTimeDomainData", domain.container) 93 | js.CopyBytesToGo(domain.Data, domain.container) 94 | } 95 | 96 | type FrequencyDataBytes struct { 97 | node js.Value // AnalyserNode value to do method call 98 | container js.Value // where to read data in JS 99 | Size int // Size of the data array 100 | Data []byte // where to copy data from JS into Go 101 | } 102 | 103 | // Update reads the current frequency data into `Data` attribute. 104 | // The frequency data is composed of integers on a scale from 0 to 255. 105 | func (freq *FrequencyDataBytes) Update() { 106 | freq.node.Call("getByteFrequencyData", freq.container) 107 | js.CopyBytesToGo(freq.Data, freq.container) 108 | } 109 | -------------------------------------------------------------------------------- /audio/audio_context.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "syscall/js" 4 | 5 | // AudioContext represents an audio-processing graph 6 | // built from audio modules linked together, each represented by an AudioNode. 7 | // An audio context controls both the creation of the nodes it contains 8 | // and the execution of the audio processing, or decoding. 9 | // You need to create an AudioContext before you do anything else, as everything happens inside a context. 10 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext 11 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext 12 | type AudioContext struct { 13 | js.Value 14 | } 15 | 16 | // GETTERS 17 | 18 | // Current time returns an ever-increasing hardware time in seconds used for scheduling. It starts at 0. 19 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/currentTime 20 | func (context AudioContext) CurrentTime() float64 { 21 | return context.Get("currentTime").Float() 22 | } 23 | 24 | // Destination is the final destination of all audio in the context. 25 | // It often represents an actual audio-rendering device such as your device's speakers. 26 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/destination 27 | func (context AudioContext) Destination() DestinationNode { 28 | node := AudioNode{Value: context.Get("destination")} 29 | return DestinationNode{AudioNode: node} 30 | } 31 | 32 | // SampleRate returns the sample rate (in samples per second) used by all nodes in this context. 33 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/sampleRate 34 | func (context AudioContext) SampleRate() int { 35 | return context.Get("sampleRate").Int() 36 | } 37 | 38 | func (context AudioContext) State() AudioContextState { 39 | return AudioContextState(context.Get("state").String()) 40 | } 41 | 42 | // METHODS 43 | 44 | func (context AudioContext) Analyser() AnalyserNode { 45 | value := context.Call("createAnalyser") 46 | node := AudioNode{Value: value} 47 | return AnalyserNode{AudioNode: node} 48 | } 49 | 50 | func (context AudioContext) BiquadFilter() BiquadFilterNode { 51 | value := context.Call("createBiquadFilter") 52 | node := AudioNode{Value: value} 53 | return BiquadFilterNode{AudioNode: node} 54 | } 55 | 56 | // Gain creates a GainNode, which can be used to control the overall volume of the audio graph. 57 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createGain 58 | // https://developer.mozilla.org/en-US/docs/Web/API/GainNode 59 | func (context AudioContext) Gain() GainNode { 60 | value := context.Call("createGain") 61 | node := AudioNode{Value: value} 62 | return GainNode{AudioNode: node} 63 | } 64 | 65 | // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createOscillator 66 | // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode 67 | func (context AudioContext) Oscillator() OscillatorNode { 68 | value := context.Call("createOscillator") 69 | node := AudioNode{Value: value} 70 | return OscillatorNode{AudioNode: node} 71 | } 72 | 73 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamSource 74 | func (context AudioContext) MediaStreamSource(stream MediaStream) MediaStreamSourceNode { 75 | value := context.Call("createMediaStreamSource", stream.JSValue()) 76 | node := AudioNode{Value: value} 77 | return MediaStreamSourceNode{AudioNode: node} 78 | } 79 | 80 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/resume 81 | func (context AudioContext) Resume() { 82 | context.Call("resume") 83 | } 84 | 85 | // SUBTYPES 86 | 87 | type AudioContextState string 88 | 89 | const ( 90 | AudioContextStateSuspended = AudioContextState("suspended") 91 | AudioContextStateRunning = AudioContextState("running") 92 | AudioContextStateClosed = AudioContextState("closed") 93 | ) 94 | -------------------------------------------------------------------------------- /audio/audio_node.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "syscall/js" 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode 6 | type AudioNode struct { 7 | js.Value 8 | } 9 | 10 | // GETTERS 11 | 12 | // Context returns the associated AudioContext, 13 | // that is the object representing the processing graph the node is participating in. 14 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/context 15 | func (node AudioNode) Context() AudioContext { 16 | return AudioContext{Value: node.Get("context")} 17 | } 18 | 19 | // Inputs returns the number of inputs feeding the node. 20 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/numberOfInputs 21 | func (node AudioNode) Inputs() int { 22 | return node.Get("numberOfInputs").Int() 23 | } 24 | 25 | // Outputs returns the number of outputs coming out of the node. 26 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/numberOfOutputs 27 | func (node AudioNode) Outputs() int { 28 | return node.Get("numberOfOutputs").Int() 29 | } 30 | 31 | func (node AudioNode) Channels() int { 32 | return node.Get("channelCount").Int() 33 | } 34 | 35 | func (node AudioNode) ChannelsMode() ChannelsMode { 36 | return ChannelsMode(node.Get("channelCountMode").String()) 37 | } 38 | 39 | func (node AudioNode) ChannelsInterpretation() ChannelsMode { 40 | return ChannelsMode(node.Get("channelCountMode").String()) 41 | } 42 | 43 | // METHODS 44 | 45 | func (node AudioNode) Connect(destination AudioNode, inputIndex int, outputIndex int) { 46 | node.Call("connect", destination.Value, outputIndex, inputIndex) 47 | } 48 | 49 | func (node AudioNode) DisconnectAll() { 50 | node.Call("disconnect") 51 | } 52 | 53 | func (node AudioNode) Disconnect(destination AudioNode) { 54 | node.Call("disconnect", destination.Value) 55 | } 56 | 57 | // SUBTYPES 58 | 59 | type Channels struct { 60 | value js.Value 61 | } 62 | 63 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/channelCount 64 | func (channels Channels) Count() int { 65 | return channels.value.Get("channelCount").Int() 66 | } 67 | 68 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/channelCountMode 69 | func (channels Channels) Mode() ChannelsMode { 70 | return ChannelsMode(channels.value.Get("channelCountMode").String()) 71 | } 72 | 73 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/channelInterpretation 74 | func (channels Channels) Discrete() bool { 75 | return channels.value.Get("channelInterpretation").String() == "discrete" 76 | } 77 | 78 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/channelInterpretation 79 | func (channels Channels) Speakers() bool { 80 | return channels.value.Get("channelInterpretation").String() == "speakers" 81 | } 82 | 83 | type ChannelsMode string 84 | 85 | const ( 86 | ChannelsModeMax = ChannelsMode("max") 87 | ChannelsModeClampedMax = ChannelsMode("clamped-max") 88 | ChannelsModeExplicit = ChannelsMode("explicit") 89 | ) 90 | -------------------------------------------------------------------------------- /audio/audio_param.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "syscall/js" 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam 6 | type AudioParam struct { 7 | value js.Value 8 | } 9 | 10 | func (param AudioParam) Default() float64 { 11 | return param.value.Get("defaultValue").Float() 12 | } 13 | 14 | func (param AudioParam) Max() float64 { 15 | return param.value.Get("maxValue").Float() 16 | } 17 | 18 | func (param AudioParam) Min() float64 { 19 | return param.value.Get("minValue").Float() 20 | } 21 | 22 | func (param AudioParam) Get() float64 { 23 | return param.value.Get("value").Float() 24 | } 25 | 26 | func (param AudioParam) Set(value float64) { 27 | param.value.Set("value", value) 28 | } 29 | 30 | // AtTime returns a namespace of operations on AudioParam 31 | // that are scheduled at specified time. 32 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam#Methods 33 | func (param AudioParam) AtTime(time float64) AtTime { 34 | return AtTime{value: param.value, time: time} 35 | } 36 | 37 | // AtTime is a namespace of operations on AudioParam 38 | // that are scheduled at specified time. 39 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam#Methods 40 | type AtTime struct { 41 | value js.Value 42 | time float64 43 | } 44 | 45 | // Set schedules an instant change to the AudioParam value at a precise time, 46 | // as measured against AudioContext.CurrentTime. 47 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setValueAtTime 48 | func (param AtTime) Set(value float64) { 49 | param.value.Call("setValueAtTime", value, param.time) 50 | } 51 | 52 | // LinearRampTo schedules a gradual linear change in the value of the AudioParam. 53 | // The change starts at the time specified for the previous event, 54 | // follows a linear ramp to the new value given in the value parameter, 55 | // and reaches the new value at the time given in the `time` parameter. 56 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/linearRampToValueAtTime 57 | func (param AtTime) LinearRampTo(value float64) { 58 | param.value.Call("linearRampToValueAtTime", value, param.time) 59 | } 60 | 61 | // ExponentialRampTo schedules a gradual exponential change in the value of the AudioParam. 62 | // The change starts at the time specified for the previous event, 63 | // follows an exponential ramp to the new value given in the value parameter, 64 | // and reaches the new value at the time given in the `time` parameter. 65 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/exponentialRampToValueAtTime 66 | func (param AtTime) ExponentialRampTo(value float64) { 67 | param.value.Call("exponentialRampToValueAtTime", value, param.time) 68 | } 69 | 70 | // SetTarget schedules the start of a gradual change to the AudioParam value. 71 | // This is useful for decay or release portions of ADSR envelopes. 72 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime 73 | func (param AtTime) SetTarget(target, timeConstant float64) { 74 | param.value.Call("setTargetAtTime", target, param.time, timeConstant) 75 | } 76 | 77 | // SetCurve schedules the parameter's value to change following a curve 78 | // defined by a list of values. The curve is a linear interpolation between 79 | // the sequence of values defined in an array of floating-point values, 80 | // which are scaled to fit into the given interval starting at `time` and a specific `duration`. 81 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setValueCurveAtTime 82 | func (param AtTime) SetCurve(values []float64, duration float64) { 83 | param.value.Call("setValueCurveAtTime", values, param.time, duration) 84 | } 85 | 86 | // Cancel cancels all scheduled future changes to the AudioParam. 87 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/cancelScheduledValues 88 | func (param AtTime) Cancel(values []float64, duration float64) { 89 | param.value.Call("cancelScheduledValues", param.time) 90 | } 91 | 92 | // CancelAndHold cancels all scheduled future changes to the AudioParam 93 | // but holds its value at a given time until further changes are made using other methods. 94 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/cancelAndHoldAtTime 95 | func (param AtTime) CancelAndHold() { 96 | param.value.Call("cancelAndHoldAtTime", param.time) 97 | } 98 | -------------------------------------------------------------------------------- /audio/biquad_filter.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode 4 | type BiquadFilterNode struct { 5 | AudioNode 6 | } 7 | 8 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/frequency 9 | func (node BiquadFilterNode) Frequency() AudioParam { 10 | return AudioParam{value: node.Get("frequency")} 11 | } 12 | 13 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/detune 14 | func (node BiquadFilterNode) DeTune() AudioParam { 15 | return AudioParam{value: node.Get("detune")} 16 | } 17 | 18 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/gain 19 | func (node BiquadFilterNode) Gain() AudioParam { 20 | return AudioParam{value: node.Get("gain")} 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/Q 24 | func (node BiquadFilterNode) QFactor() AudioParam { 25 | return AudioParam{value: node.Get("Q")} 26 | } 27 | 28 | // FilterType is kind of filtering algorithm the node is implementing 29 | // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode/type 30 | func (node BiquadFilterNode) FilterType() FilterType { 31 | return FilterType(node.Get("type").String()) 32 | } 33 | 34 | // SUBTYPES 35 | 36 | type FilterType string 37 | 38 | const ( 39 | FilterTypeLowPass = FilterType("lowpass") 40 | FilterTypeHighPass = FilterType("highpass") 41 | FilterTypeBandPass = FilterType("bandpass") 42 | FilterTypeLowShelf = FilterType("lowshelf") 43 | FilterTypeHighShelf = FilterType("highshelf") 44 | FilterTypePeaking = FilterType("peaking") 45 | FilterTypeNotch = FilterType("notch") 46 | FilterTypeAllPass = FilterType("allpass") 47 | ) 48 | -------------------------------------------------------------------------------- /audio/destination_node.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | type DestinationNode struct { 4 | AudioNode 5 | } 6 | 7 | func (node DestinationNode) MaxChannels() int { 8 | return node.Get("maxChannelCount").Int() 9 | } 10 | -------------------------------------------------------------------------------- /audio/gain_node.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // GainNode represents a change in volume. 4 | // It is an AudioNode audio-processing module that causes 5 | // a given gain to be applied to the input data before its propagation 6 | // to the output. A GainNode always has exactly one input and one output, 7 | // both with the same number of channels. 8 | // https://developer.mozilla.org/en-US/docs/Web/API/GainNode 9 | type GainNode struct { 10 | AudioNode 11 | } 12 | 13 | func (node GainNode) Gain() AudioParam { 14 | return AudioParam{value: node.Get("gain")} 15 | } 16 | -------------------------------------------------------------------------------- /audio/media_stream.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "syscall/js" 4 | 5 | type MediaStream struct { 6 | js.Value 7 | } 8 | 9 | // Casts audio.MediaStream to js.Value 10 | func (stream MediaStream) JSValue() js.Value { 11 | return stream.Value 12 | } 13 | 14 | // PROPERTIES 15 | 16 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/active 17 | func (stream MediaStream) Active() bool { 18 | return stream.Get("active").Bool() 19 | } 20 | 21 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/id 22 | func (stream MediaStream) ID() string { 23 | return stream.Get("active").String() 24 | } 25 | 26 | // METHODS 27 | 28 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/clone 29 | func (stream MediaStream) Clone() MediaStream { 30 | return MediaStream{Value: stream.Call("clone")} 31 | } 32 | -------------------------------------------------------------------------------- /audio/media_stream_source_node.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamAudioSourceNode 4 | type MediaStreamSourceNode struct { 5 | AudioNode 6 | } 7 | 8 | func (node MediaStreamSourceNode) Stream() MediaStream { 9 | return MediaStream{Value: node.Get("mediaStream")} 10 | } 11 | -------------------------------------------------------------------------------- /audio/oscillator.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode 4 | type OscillatorNode struct { 5 | AudioNode 6 | } 7 | 8 | // PROPERTIES 9 | 10 | func (node OscillatorNode) Frequency() AudioParam { 11 | return AudioParam{value: node.Get("frequency")} 12 | } 13 | 14 | func (node OscillatorNode) DeTune() AudioParam { 15 | return AudioParam{value: node.Get("detune")} 16 | } 17 | 18 | // Shape specifies the shape of waveform to play. 19 | // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type 20 | func (node OscillatorNode) Shape() Shape { 21 | return Shape(node.Get("type").String()) 22 | } 23 | 24 | func (node OscillatorNode) SetShape(shape Shape) { 25 | node.Set("type", string(shape)) 26 | } 27 | 28 | // METHODS 29 | 30 | // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/start 31 | func (node OscillatorNode) Start(when float64) { 32 | node.Call("start", when) 33 | } 34 | 35 | // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/stop 36 | func (node OscillatorNode) Stop(when float64) { 37 | node.Call("stop", when) 38 | } 39 | 40 | // SUBTYPES 41 | 42 | type Shape string 43 | 44 | const ( 45 | ShapeSine = Shape("sine") 46 | ShapeSquare = Shape("square") 47 | ShapeSawTooth = Shape("sawtooth") 48 | ShapeTriangle = Shape("triangle") 49 | ShapeCustom = Shape("custom") 50 | ) 51 | -------------------------------------------------------------------------------- /audio/value.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "syscall/js" 5 | ) 6 | 7 | // Value is an extended js.Value with more types support 8 | type Value struct { 9 | js.Value 10 | } 11 | 12 | // overloaded methods 13 | 14 | func (v Value) Call(method string, args ...any) Value { 15 | result := v.Value.Call(method, args...) 16 | return Value{Value: result} 17 | } 18 | 19 | func (v Value) Get(property string) Value { 20 | result := v.Value.Get(property) 21 | return Value{Value: result} 22 | } 23 | 24 | func (v Value) New(args ...any) Value { 25 | result := v.Value.New(args...) 26 | return Value{Value: result} 27 | } 28 | 29 | // new methods 30 | 31 | func (v *Value) Values() (items []Value) { 32 | len := v.Get("length").Int() 33 | for i := 0; i < len; i++ { 34 | item := v.Call("item", i) 35 | items = append(items, item) 36 | } 37 | return items 38 | } 39 | 40 | func (v Value) Strings() (items []string) { 41 | len := v.Get("length").Int() 42 | for i := 0; i < len; i++ { 43 | item := v.Call("item", i) 44 | items = append(items, item.String()) 45 | } 46 | return items 47 | } 48 | 49 | // OptionalString returns empty string if Value is null 50 | func (v Value) OptionalString() string { 51 | switch v.Type() { 52 | case js.TypeNull: 53 | return "" 54 | case js.TypeString: 55 | return v.String() 56 | default: 57 | panic("bad type") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /canvas/context.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Context struct { 6 | js.Value 7 | } 8 | 9 | func (context Context) Context2D() Context2D { 10 | return Context2D{value: context.Value} 11 | } 12 | -------------------------------------------------------------------------------- /canvas/context2d.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Context2D struct { 6 | value js.Value 7 | } 8 | 9 | // SUBTYPE GETTERS 10 | 11 | func (context Context2D) Shadow() Shadow { 12 | return Shadow(context) 13 | } 14 | 15 | func (context Context2D) Line() Line { 16 | return Line(context) 17 | } 18 | 19 | func (context Context2D) Rectangle(x, y, width, height int) Rectangle { 20 | return Rectangle{value: context.value, x: x, y: y, width: width, height: height} 21 | } 22 | 23 | func (context Context2D) Text() Text { 24 | return Text(context) 25 | } 26 | 27 | // STYLES 28 | 29 | func (context Context2D) FillStyle() string { 30 | return context.value.Get("fillStyle").String() 31 | } 32 | 33 | func (context Context2D) SetFillStyle(value string) { 34 | context.value.Set("fillStyle", value) 35 | } 36 | 37 | func (context Context2D) StrokeStyle() string { 38 | return context.value.Get("strokeStyle").String() 39 | } 40 | 41 | func (context Context2D) SetStrokeStyle(value string) { 42 | context.value.Set("strokeStyle", value) 43 | } 44 | 45 | // OTHER ATTRS 46 | 47 | // GlobalAlpha returns the current alpha or transparency value of the drawing 48 | func (context Context2D) GlobalAlpha() float64 { 49 | return context.value.Get("globalAlpha").Float() 50 | } 51 | 52 | func (context Context2D) SetGlobalAlpha(value float64) { 53 | context.value.Set("globalAlpha", value) 54 | } 55 | 56 | func (context Context2D) GlobalCompositeOperation() string { 57 | return context.value.Get("globalCompositeOperation").String() 58 | } 59 | 60 | func (context Context2D) SetGlobalCompositeOperation(value string) { 61 | context.value.Set("globalCompositeOperation", value) 62 | } 63 | 64 | // PATH API 65 | 66 | func (context Context2D) BeginPath() { 67 | context.value.Call("beginPath") 68 | } 69 | 70 | func (context Context2D) ClosePath() { 71 | context.value.Call("closePath") 72 | } 73 | 74 | func (context Context2D) Arc(x, y, r int, sAngle, eAngle float64) { 75 | context.value.Call("arc", x, y, r, sAngle, eAngle, false) 76 | } 77 | 78 | func (context Context2D) ArcTo(x1, y1, x2, y2, r int) { 79 | context.value.Call("arcTo", x1, y1, x2, y2, r) 80 | } 81 | 82 | func (context Context2D) Clip() { 83 | context.value.Call("clip") 84 | } 85 | 86 | func (context Context2D) Fill() { 87 | context.value.Call("fill") 88 | } 89 | 90 | // IsPointInPath returns true if the specified point is in the current path 91 | func (context Context2D) IsPointInPath(x int, y int) bool { 92 | return context.value.Call("isPointInPath", x, y).Bool() 93 | } 94 | 95 | func (context Context2D) LineTo(x int, y int) { 96 | context.value.Call("lineTo", x, y) 97 | } 98 | 99 | func (context Context2D) MoveTo(x int, y int) { 100 | context.value.Call("moveTo", x, y) 101 | } 102 | 103 | func (context Context2D) Stroke() { 104 | context.value.Call("stroke") 105 | } 106 | 107 | // BezierCurveTo adds a point to the current path by using the specified 108 | // control points that represent a cubic Bézier curve. 109 | func (context Context2D) BezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y int) { 110 | context.value.Call("bezierCurveTo", cp1x, cp1y, cp2x, cp2y, x, y) 111 | } 112 | 113 | // QuadraticCurveTo adds a point to the current path by using the specified 114 | // control points that represent a quadratic Bézier curve. 115 | func (context Context2D) QuadraticCurveTo(cpx, cpy, x, y int) { 116 | context.value.Call("quadraticCurveTo", cpx, cpy, x, y) 117 | } 118 | 119 | // TRANSFORMATION API 120 | 121 | // Rotate rotates the current drawing 122 | func (context Context2D) Rotate(angle float64) { 123 | context.value.Call("scale", angle) 124 | } 125 | 126 | // Scale scales the current drawing bigger or smaller 127 | func (context Context2D) Scale(x float64, y float64) { 128 | context.value.Call("scale", x, y) 129 | } 130 | 131 | // Transform replaces the current transformation matrix. 132 | // a: Horizontal scaling 133 | // b: Horizontal skewing 134 | // c: Vertical skewing 135 | // d: Vertical scaling 136 | // e: Horizontal moving 137 | // f: Vertical moving 138 | func (context Context2D) Transform(a, b, c, d, e, f float64) { 139 | context.value.Call("transform", a, b, c, d, e, f) 140 | } 141 | 142 | // Translate remaps the (0,0) position on the canvas 143 | func (context Context2D) Translate(x float64, y float64) { 144 | context.value.Call("translate", x, y) 145 | } 146 | -------------------------------------------------------------------------------- /canvas/line.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Line struct { 6 | value js.Value 7 | } 8 | 9 | func (context Line) Cap() string { 10 | return context.value.Get("lineCap").String() 11 | } 12 | 13 | func (context Line) SetCap(value string) { 14 | context.value.Set("lineCap", value) 15 | } 16 | 17 | func (context Line) Join() string { 18 | return context.value.Get("lineJoin").String() 19 | } 20 | 21 | func (context Line) SetJoin(value string) { 22 | context.value.Set("lineJoin", value) 23 | } 24 | 25 | func (context Line) MiterLimit() string { 26 | return context.value.Get("miterLimit").String() 27 | } 28 | 29 | func (context Line) SetMiterLimit(value string) { 30 | context.value.Set("miterLimit", value) 31 | } 32 | 33 | func (context Line) Width() int { 34 | return context.value.Get("lineWidth").Int() 35 | } 36 | 37 | func (context Line) SetWidth(value int) { 38 | context.value.Set("lineWidth", value) 39 | } 40 | 41 | func (context Line) Draw(x1, y1, x2, y2 int) { 42 | context.value.Call("beginPath") 43 | context.value.Call("moveTo", x1, y1) 44 | context.value.Call("lineTo", x2, y2) 45 | context.value.Call("stroke") 46 | } 47 | -------------------------------------------------------------------------------- /canvas/rectangle.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Rectangle struct { 6 | value js.Value 7 | 8 | x, y, width, height int 9 | 10 | cleared bool 11 | filled bool 12 | stroked bool 13 | corners int 14 | } 15 | 16 | // set params 17 | 18 | func (rect Rectangle) Cleared() Rectangle { 19 | rect.cleared = true 20 | return rect 21 | } 22 | 23 | func (rect Rectangle) Filled() Rectangle { 24 | rect.filled = true 25 | return rect 26 | } 27 | 28 | func (rect Rectangle) Stroked() Rectangle { 29 | rect.stroked = true 30 | return rect 31 | } 32 | 33 | func (rect Rectangle) Rounded(radius int) Rectangle { 34 | rect.corners = radius 35 | return rect 36 | } 37 | 38 | // draw 39 | 40 | func (rect Rectangle) Draw() { 41 | if rect.corners > 0 { 42 | rect.drawRoundedStroke() 43 | return 44 | } 45 | 46 | // clear 47 | if rect.cleared { 48 | rect.value.Call("clearRect", rect.x, rect.y, rect.width, rect.height) 49 | return 50 | } 51 | // stroke and fill 52 | if rect.stroked && rect.filled { 53 | rect.value.Call("rect", rect.x, rect.y, rect.width, rect.height) 54 | return 55 | } 56 | // only fill 57 | if rect.filled { 58 | rect.value.Call("fillRect", rect.x, rect.y, rect.width, rect.height) 59 | return 60 | } 61 | // only stroke (default) 62 | rect.value.Call("strokeRect", rect.x, rect.y, rect.width, rect.height) 63 | } 64 | 65 | func (rect Rectangle) drawRoundedStroke() { 66 | top := rect.y + rect.height 67 | right := rect.x + rect.width 68 | rad := rect.corners 69 | 70 | rect.value.Call("beginPath") 71 | rect.value.Call("moveTo", rect.x, rect.y+rad) 72 | rect.value.Call("lineTo", rect.x, top-rad) 73 | rect.value.Call("arcTo", rect.x, top, rect.x+rad, top, rad) 74 | rect.value.Call("lineTo", right-rad, top) 75 | rect.value.Call("arcTo", right, top, right, top-rad, rad) 76 | rect.value.Call("lineTo", right, rect.y+rad) 77 | rect.value.Call("arcTo", right, rect.y, right-rad, rect.y, rad) 78 | rect.value.Call("lineTo", rect.x+rad, rect.y) 79 | rect.value.Call("arcTo", rect.x, rect.y, rect.x, rect.y+rad, rad) 80 | rect.value.Call("stroke") 81 | } 82 | -------------------------------------------------------------------------------- /canvas/shadow.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Shadow struct { 6 | value js.Value 7 | } 8 | 9 | func (context Shadow) Blur() float64 { 10 | return context.value.Get("shadowBlur").Float() 11 | } 12 | 13 | func (context Shadow) SetBlur(value float64) { 14 | context.value.Set("shadowBlur", value) 15 | } 16 | 17 | func (context Shadow) Color() string { 18 | return context.value.Get("shadowColor").String() 19 | } 20 | 21 | func (context Shadow) SetColor(value string) { 22 | context.value.Set("shadowColor", value) 23 | } 24 | 25 | func (context Shadow) OffsetX() float64 { 26 | return context.value.Get("shadowOffsetX").Float() 27 | } 28 | 29 | func (context Shadow) SetOffsetX(value float64) { 30 | context.value.Set("shadowOffsetX", value) 31 | } 32 | 33 | func (context Shadow) OffsetY() float64 { 34 | return context.value.Get("shadowOffsetY").Float() 35 | } 36 | 37 | func (context Shadow) SetOffsetY(value float64) { 38 | context.value.Set("shadowOffsetY", value) 39 | } 40 | -------------------------------------------------------------------------------- /canvas/text.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import "syscall/js" 4 | 5 | type Text struct { 6 | value js.Value 7 | } 8 | 9 | func (context Text) Align() string { 10 | return context.value.Get("align").String() 11 | } 12 | 13 | func (context Text) SetAlign(value string) { 14 | context.value.Set("align", value) 15 | } 16 | 17 | func (context Text) Baseline() string { 18 | return context.value.Get("baseline").String() 19 | } 20 | 21 | func (context Text) SetBaseline(value string) { 22 | context.value.Set("baseline", value) 23 | } 24 | 25 | func (context Text) Font() string { 26 | return context.value.Get("font").String() 27 | } 28 | 29 | func (context Text) SetFont(value string) { 30 | context.value.Set("font", value) 31 | } 32 | 33 | func (context Text) Fill(text string, x, y, maxWidth int) { 34 | if maxWidth <= 0 { 35 | context.value.Call("fillText", text, x, y) 36 | } else { 37 | context.value.Call("fillText", text, x, y, maxWidth) 38 | } 39 | } 40 | 41 | func (context Text) Stroke(text string, x, y, maxWidth int) { 42 | if maxWidth == 0 { 43 | context.value.Call("strokeText", x, y) 44 | } else { 45 | context.value.Call("strokeText", x, y, maxWidth) 46 | } 47 | } 48 | 49 | func (context Text) Width(text string) int { 50 | return context.value.Call("measureText", text).Get("width").Int() 51 | } 52 | -------------------------------------------------------------------------------- /css/style_declaration.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import "syscall/js" 4 | 5 | type CSSStyleDeclaration struct { 6 | Value js.Value 7 | } 8 | 9 | // RULES MANIPULATION 10 | 11 | func (decl CSSStyleDeclaration) Len() int { 12 | return decl.Value.Get("length").Int() 13 | } 14 | 15 | func (decl CSSStyleDeclaration) Names() []string { 16 | length := decl.Len() 17 | items := make([]string, length) 18 | for i := 0; i < length; i++ { 19 | items[i] = decl.Value.Call("item", i).String() 20 | } 21 | return items 22 | } 23 | 24 | func (decl CSSStyleDeclaration) Get(name string) string { 25 | return decl.Value.Call("getPropertyValue", name).String() 26 | } 27 | 28 | func (decl CSSStyleDeclaration) Set(name, value string, important bool) { 29 | priority := "" 30 | if important { 31 | priority = "important" 32 | } 33 | decl.Value.Call("setProperty", name, value, priority) 34 | } 35 | 36 | func (decl CSSStyleDeclaration) Remove(name string) { 37 | decl.Value.Call("removeProperty", name) 38 | } 39 | 40 | func (decl CSSStyleDeclaration) Important(name string) bool { 41 | return decl.Value.Call("getPropertyPriority", name).String() == "important" 42 | } 43 | 44 | // RULES GETTERS 45 | 46 | func (decl CSSStyleDeclaration) Background() string { 47 | return decl.Get("background") 48 | } 49 | 50 | func (decl CSSStyleDeclaration) BackgroundAttachment() string { 51 | return decl.Get("background-attachment") 52 | } 53 | 54 | func (decl CSSStyleDeclaration) BackgroundColor() string { 55 | return decl.Get("background-color") 56 | } 57 | 58 | func (decl CSSStyleDeclaration) BackgroundImage() string { 59 | return decl.Get("background-image") 60 | } 61 | 62 | func (decl CSSStyleDeclaration) BackgroundPosition() string { 63 | return decl.Get("background-position") 64 | } 65 | 66 | func (decl CSSStyleDeclaration) BackgroundRepeat() string { 67 | return decl.Get("background-repeat") 68 | } 69 | 70 | func (decl CSSStyleDeclaration) Border() string { 71 | return decl.Get("border") 72 | } 73 | 74 | func (decl CSSStyleDeclaration) BorderBottom() string { 75 | return decl.Get("border-bottom") 76 | } 77 | 78 | func (decl CSSStyleDeclaration) BorderBottomColor() string { 79 | return decl.Get("border-bottom-color") 80 | } 81 | 82 | func (decl CSSStyleDeclaration) BorderBottomStyle() string { 83 | return decl.Get("border-bottom-style") 84 | } 85 | 86 | func (decl CSSStyleDeclaration) BorderBottomWidth() string { 87 | return decl.Get("border-bottom-width") 88 | } 89 | 90 | func (decl CSSStyleDeclaration) BorderColor() string { 91 | return decl.Get("border-color") 92 | } 93 | 94 | func (decl CSSStyleDeclaration) BorderLeft() string { 95 | return decl.Get("border-left") 96 | } 97 | 98 | func (decl CSSStyleDeclaration) BorderLeftColor() string { 99 | return decl.Get("border-left-color") 100 | } 101 | 102 | func (decl CSSStyleDeclaration) BorderLeftStyle() string { 103 | return decl.Get("border-left-style") 104 | } 105 | 106 | func (decl CSSStyleDeclaration) BorderLeftWidth() string { 107 | return decl.Get("border-left-width") 108 | } 109 | 110 | func (decl CSSStyleDeclaration) BorderRight() string { 111 | return decl.Get("border-right") 112 | } 113 | 114 | func (decl CSSStyleDeclaration) BorderRightColor() string { 115 | return decl.Get("border-right-color") 116 | } 117 | 118 | func (decl CSSStyleDeclaration) BorderRightStyle() string { 119 | return decl.Get("border-right-style") 120 | } 121 | 122 | func (decl CSSStyleDeclaration) BorderRightWidth() string { 123 | return decl.Get("border-right-width") 124 | } 125 | 126 | func (decl CSSStyleDeclaration) BorderStyle() string { 127 | return decl.Get("border-style") 128 | } 129 | 130 | func (decl CSSStyleDeclaration) BorderTop() string { 131 | return decl.Get("border-top") 132 | } 133 | 134 | func (decl CSSStyleDeclaration) BorderTopColor() string { 135 | return decl.Get("border-top-color") 136 | } 137 | 138 | func (decl CSSStyleDeclaration) BorderTopStyle() string { 139 | return decl.Get("border-top-style") 140 | } 141 | 142 | func (decl CSSStyleDeclaration) BorderTopWidth() string { 143 | return decl.Get("border-top-width") 144 | } 145 | 146 | func (decl CSSStyleDeclaration) BorderWidth() string { 147 | return decl.Get("border-width") 148 | } 149 | 150 | func (decl CSSStyleDeclaration) Clear() string { 151 | return decl.Get("clear") 152 | } 153 | 154 | func (decl CSSStyleDeclaration) Clip() string { 155 | return decl.Get("clip") 156 | } 157 | 158 | func (decl CSSStyleDeclaration) Color() string { 159 | return decl.Get("color") 160 | } 161 | 162 | func (decl CSSStyleDeclaration) Cursor() string { 163 | return decl.Get("cursor") 164 | } 165 | 166 | func (decl CSSStyleDeclaration) Display() string { 167 | return decl.Get("display") 168 | } 169 | 170 | func (decl CSSStyleDeclaration) Filter() string { 171 | return decl.Get("filter") 172 | } 173 | 174 | func (decl CSSStyleDeclaration) Float() string { 175 | return decl.Get("float") 176 | } 177 | 178 | func (decl CSSStyleDeclaration) Font() string { 179 | return decl.Get("font") 180 | } 181 | 182 | func (decl CSSStyleDeclaration) FontFamily() string { 183 | return decl.Get("font-family") 184 | } 185 | 186 | func (decl CSSStyleDeclaration) FontSize() string { 187 | return decl.Get("font-size") 188 | } 189 | 190 | func (decl CSSStyleDeclaration) FontVariant() string { 191 | return decl.Get("font-variant") 192 | } 193 | 194 | func (decl CSSStyleDeclaration) FontWeight() string { 195 | return decl.Get("font-weight") 196 | } 197 | 198 | func (decl CSSStyleDeclaration) Height() string { 199 | return decl.Get("height") 200 | } 201 | 202 | func (decl CSSStyleDeclaration) Left() string { 203 | return decl.Get("left") 204 | } 205 | 206 | func (decl CSSStyleDeclaration) LetterSpacing() string { 207 | return decl.Get("letter-spacing") 208 | } 209 | 210 | func (decl CSSStyleDeclaration) LineHeight() string { 211 | return decl.Get("line-height") 212 | } 213 | 214 | func (decl CSSStyleDeclaration) ListStyle() string { 215 | return decl.Get("list-style") 216 | } 217 | 218 | func (decl CSSStyleDeclaration) ListStyleImage() string { 219 | return decl.Get("list-style-image") 220 | } 221 | 222 | func (decl CSSStyleDeclaration) ListStylePosition() string { 223 | return decl.Get("list-style-position") 224 | } 225 | 226 | func (decl CSSStyleDeclaration) ListStyleType() string { 227 | return decl.Get("list-style-type") 228 | } 229 | 230 | func (decl CSSStyleDeclaration) Margin() string { 231 | return decl.Get("margin") 232 | } 233 | 234 | func (decl CSSStyleDeclaration) MarginBottom() string { 235 | return decl.Get("margin-bottom") 236 | } 237 | 238 | func (decl CSSStyleDeclaration) MarginLeft() string { 239 | return decl.Get("margin-left") 240 | } 241 | 242 | func (decl CSSStyleDeclaration) MarginRight() string { 243 | return decl.Get("margin-right") 244 | } 245 | 246 | func (decl CSSStyleDeclaration) MarginTop() string { 247 | return decl.Get("margin-top") 248 | } 249 | 250 | func (decl CSSStyleDeclaration) Overflow() string { 251 | return decl.Get("overflow") 252 | } 253 | 254 | func (decl CSSStyleDeclaration) Padding() string { 255 | return decl.Get("padding") 256 | } 257 | 258 | func (decl CSSStyleDeclaration) PaddingBottom() string { 259 | return decl.Get("padding-bottom") 260 | } 261 | 262 | func (decl CSSStyleDeclaration) PaddingLeft() string { 263 | return decl.Get("padding-left") 264 | } 265 | 266 | func (decl CSSStyleDeclaration) PaddingRight() string { 267 | return decl.Get("padding-right") 268 | } 269 | 270 | func (decl CSSStyleDeclaration) PaddingTop() string { 271 | return decl.Get("padding-top") 272 | } 273 | 274 | func (decl CSSStyleDeclaration) PageBreakAfter() string { 275 | return decl.Get("page-break-after") 276 | } 277 | 278 | func (decl CSSStyleDeclaration) PageBreakBefore() string { 279 | return decl.Get("page-break-before") 280 | } 281 | 282 | func (decl CSSStyleDeclaration) Position() string { 283 | return decl.Get("position") 284 | } 285 | 286 | func (decl CSSStyleDeclaration) StrokeDasharray() string { 287 | return decl.Get("stroke-dasharray") 288 | } 289 | 290 | func (decl CSSStyleDeclaration) StrokeDashoffset() string { 291 | return decl.Get("stroke-dashoffset") 292 | } 293 | 294 | func (decl CSSStyleDeclaration) StrokeWidth() string { 295 | return decl.Get("stroke-width") 296 | } 297 | 298 | func (decl CSSStyleDeclaration) TextAlign() string { 299 | return decl.Get("text-align") 300 | } 301 | 302 | func (decl CSSStyleDeclaration) TextDecoration() string { 303 | return decl.Get("text-decoration") 304 | } 305 | 306 | func (decl CSSStyleDeclaration) TextIndent() string { 307 | return decl.Get("text-indent") 308 | } 309 | 310 | func (decl CSSStyleDeclaration) TextTransform() string { 311 | return decl.Get("text-transform") 312 | } 313 | 314 | func (decl CSSStyleDeclaration) Top() string { 315 | return decl.Get("top") 316 | } 317 | 318 | func (decl CSSStyleDeclaration) VerticalAlign() string { 319 | return decl.Get("vertical-align") 320 | } 321 | 322 | func (decl CSSStyleDeclaration) Visibility() string { 323 | return decl.Get("visibility") 324 | } 325 | 326 | func (decl CSSStyleDeclaration) Width() string { 327 | return decl.Get("width") 328 | } 329 | 330 | func (decl CSSStyleDeclaration) ZIndex() string { 331 | return decl.Get("z-index") 332 | } 333 | 334 | // SETTERS 335 | 336 | func (decl CSSStyleDeclaration) SetBackground(value string, important bool) { 337 | decl.Set("background", value, important) 338 | } 339 | 340 | func (decl CSSStyleDeclaration) SetBackgroundAttachment(value string, important bool) { 341 | decl.Set("background-attachment", value, important) 342 | } 343 | 344 | func (decl CSSStyleDeclaration) SetBackgroundColor(value string, important bool) { 345 | decl.Set("background-color", value, important) 346 | } 347 | 348 | func (decl CSSStyleDeclaration) SetBackgroundImage(value string, important bool) { 349 | decl.Set("background-image", value, important) 350 | } 351 | 352 | func (decl CSSStyleDeclaration) SetBackgroundPosition(value string, important bool) { 353 | decl.Set("background-position", value, important) 354 | } 355 | 356 | func (decl CSSStyleDeclaration) SetBackgroundRepeat(value string, important bool) { 357 | decl.Set("background-repeat", value, important) 358 | } 359 | 360 | func (decl CSSStyleDeclaration) SetBorder(value string, important bool) { 361 | decl.Set("border", value, important) 362 | } 363 | 364 | func (decl CSSStyleDeclaration) SetBorderBottom(value string, important bool) { 365 | decl.Set("border-bottom", value, important) 366 | } 367 | 368 | func (decl CSSStyleDeclaration) SetBorderBottomColor(value string, important bool) { 369 | decl.Set("border-bottom-color", value, important) 370 | } 371 | 372 | func (decl CSSStyleDeclaration) SetBorderBottomStyle(value string, important bool) { 373 | decl.Set("border-bottom-style", value, important) 374 | } 375 | 376 | func (decl CSSStyleDeclaration) SetBorderBottomWidth(value string, important bool) { 377 | decl.Set("border-bottom-width", value, important) 378 | } 379 | 380 | func (decl CSSStyleDeclaration) SetBorderColor(value string, important bool) { 381 | decl.Set("border-color", value, important) 382 | } 383 | 384 | func (decl CSSStyleDeclaration) SetBorderLeft(value string, important bool) { 385 | decl.Set("border-left", value, important) 386 | } 387 | 388 | func (decl CSSStyleDeclaration) SetBorderLeftColor(value string, important bool) { 389 | decl.Set("border-left-color", value, important) 390 | } 391 | 392 | func (decl CSSStyleDeclaration) SetBorderLeftStyle(value string, important bool) { 393 | decl.Set("border-left-style", value, important) 394 | } 395 | 396 | func (decl CSSStyleDeclaration) SetBorderLeftWidth(value string, important bool) { 397 | decl.Set("border-left-width", value, important) 398 | } 399 | 400 | func (decl CSSStyleDeclaration) SetBorderRight(value string, important bool) { 401 | decl.Set("border-right", value, important) 402 | } 403 | 404 | func (decl CSSStyleDeclaration) SetBorderRightColor(value string, important bool) { 405 | decl.Set("border-right-color", value, important) 406 | } 407 | 408 | func (decl CSSStyleDeclaration) SetBorderRightStyle(value string, important bool) { 409 | decl.Set("border-right-style", value, important) 410 | } 411 | 412 | func (decl CSSStyleDeclaration) SetBorderRightWidth(value string, important bool) { 413 | decl.Set("border-right-width", value, important) 414 | } 415 | 416 | func (decl CSSStyleDeclaration) SetBorderStyle(value string, important bool) { 417 | decl.Set("border-style", value, important) 418 | } 419 | 420 | func (decl CSSStyleDeclaration) SetBorderTop(value string, important bool) { 421 | decl.Set("border-top", value, important) 422 | } 423 | 424 | func (decl CSSStyleDeclaration) SetBorderTopColor(value string, important bool) { 425 | decl.Set("border-top-color", value, important) 426 | } 427 | 428 | func (decl CSSStyleDeclaration) SetBorderTopStyle(value string, important bool) { 429 | decl.Set("border-top-style", value, important) 430 | } 431 | 432 | func (decl CSSStyleDeclaration) SetBorderTopWidth(value string, important bool) { 433 | decl.Set("border-top-width", value, important) 434 | } 435 | 436 | func (decl CSSStyleDeclaration) SetBorderWidth(value string, important bool) { 437 | decl.Set("border-width", value, important) 438 | } 439 | 440 | func (decl CSSStyleDeclaration) SetClear(value string, important bool) { 441 | decl.Set("clear", value, important) 442 | } 443 | 444 | func (decl CSSStyleDeclaration) SetClip(value string, important bool) { 445 | decl.Set("clip", value, important) 446 | } 447 | 448 | func (decl CSSStyleDeclaration) SetColor(value string, important bool) { 449 | decl.Set("color", value, important) 450 | } 451 | 452 | func (decl CSSStyleDeclaration) SetCursor(value string, important bool) { 453 | decl.Set("cursor", value, important) 454 | } 455 | 456 | func (decl CSSStyleDeclaration) SetDisplay(value string, important bool) { 457 | decl.Set("display", value, important) 458 | } 459 | 460 | func (decl CSSStyleDeclaration) SetFilter(value string, important bool) { 461 | decl.Set("filter", value, important) 462 | } 463 | 464 | func (decl CSSStyleDeclaration) SetFloat(value string, important bool) { 465 | decl.Set("float", value, important) 466 | } 467 | 468 | func (decl CSSStyleDeclaration) SetFont(value string, important bool) { 469 | decl.Set("font", value, important) 470 | } 471 | 472 | func (decl CSSStyleDeclaration) SetFontFamily(value string, important bool) { 473 | decl.Set("font-family", value, important) 474 | } 475 | 476 | func (decl CSSStyleDeclaration) SetFontSize(value string, important bool) { 477 | decl.Set("font-size", value, important) 478 | } 479 | 480 | func (decl CSSStyleDeclaration) SetFontVariant(value string, important bool) { 481 | decl.Set("font-variant", value, important) 482 | } 483 | 484 | func (decl CSSStyleDeclaration) SetFontWeight(value string, important bool) { 485 | decl.Set("font-weight", value, important) 486 | } 487 | 488 | func (decl CSSStyleDeclaration) SetHeight(value string, important bool) { 489 | decl.Set("height", value, important) 490 | } 491 | 492 | func (decl CSSStyleDeclaration) SetLeft(value string, important bool) { 493 | decl.Set("left", value, important) 494 | } 495 | 496 | func (decl CSSStyleDeclaration) SetLetterSpacing(value string, important bool) { 497 | decl.Set("letter-spacing", value, important) 498 | } 499 | 500 | func (decl CSSStyleDeclaration) SetLineHeight(value string, important bool) { 501 | decl.Set("line-height", value, important) 502 | } 503 | 504 | func (decl CSSStyleDeclaration) SetListStyle(value string, important bool) { 505 | decl.Set("list-style", value, important) 506 | } 507 | 508 | func (decl CSSStyleDeclaration) SetListStyleImage(value string, important bool) { 509 | decl.Set("list-style-image", value, important) 510 | } 511 | 512 | func (decl CSSStyleDeclaration) SetListStylePosition(value string, important bool) { 513 | decl.Set("list-style-position", value, important) 514 | } 515 | 516 | func (decl CSSStyleDeclaration) SetListStyleType(value string, important bool) { 517 | decl.Set("list-style-type", value, important) 518 | } 519 | 520 | func (decl CSSStyleDeclaration) SetMargin(value string, important bool) { 521 | decl.Set("margin", value, important) 522 | } 523 | 524 | func (decl CSSStyleDeclaration) SetMarginBottom(value string, important bool) { 525 | decl.Set("margin-bottom", value, important) 526 | } 527 | 528 | func (decl CSSStyleDeclaration) SetMarginLeft(value string, important bool) { 529 | decl.Set("margin-left", value, important) 530 | } 531 | 532 | func (decl CSSStyleDeclaration) SetMarginRight(value string, important bool) { 533 | decl.Set("margin-right", value, important) 534 | } 535 | 536 | func (decl CSSStyleDeclaration) SetMarginTop(value string, important bool) { 537 | decl.Set("margin-top", value, important) 538 | } 539 | 540 | func (decl CSSStyleDeclaration) SetOverflow(value string, important bool) { 541 | decl.Set("overflow", value, important) 542 | } 543 | 544 | func (decl CSSStyleDeclaration) SetPadding(value string, important bool) { 545 | decl.Set("padding", value, important) 546 | } 547 | 548 | func (decl CSSStyleDeclaration) SetPaddingBottom(value string, important bool) { 549 | decl.Set("padding-bottom", value, important) 550 | } 551 | 552 | func (decl CSSStyleDeclaration) SetPaddingLeft(value string, important bool) { 553 | decl.Set("padding-left", value, important) 554 | } 555 | 556 | func (decl CSSStyleDeclaration) SetPaddingRight(value string, important bool) { 557 | decl.Set("padding-right", value, important) 558 | } 559 | 560 | func (decl CSSStyleDeclaration) SetPaddingTop(value string, important bool) { 561 | decl.Set("padding-top", value, important) 562 | } 563 | 564 | func (decl CSSStyleDeclaration) SetPageBreakAfter(value string, important bool) { 565 | decl.Set("page-break-after", value, important) 566 | } 567 | 568 | func (decl CSSStyleDeclaration) SetPageBreakBefore(value string, important bool) { 569 | decl.Set("page-break-before", value, important) 570 | } 571 | 572 | func (decl CSSStyleDeclaration) SetPosition(value string, important bool) { 573 | decl.Set("position", value, important) 574 | } 575 | 576 | func (decl CSSStyleDeclaration) SetStrokeDasharray(value string, important bool) { 577 | decl.Set("stroke-dasharray", value, important) 578 | } 579 | 580 | func (decl CSSStyleDeclaration) SetStrokeDashoffset(value string, important bool) { 581 | decl.Set("stroke-dashoffset", value, important) 582 | } 583 | 584 | func (decl CSSStyleDeclaration) SetStrokeWidth(value string, important bool) { 585 | decl.Set("stroke-width", value, important) 586 | } 587 | 588 | func (decl CSSStyleDeclaration) SetTextAlign(value string, important bool) { 589 | decl.Set("text-align", value, important) 590 | } 591 | 592 | func (decl CSSStyleDeclaration) SetTextDecoration(value string, important bool) { 593 | decl.Set("text-decoration", value, important) 594 | } 595 | 596 | func (decl CSSStyleDeclaration) SetTextIndent(value string, important bool) { 597 | decl.Set("text-indent", value, important) 598 | } 599 | 600 | func (decl CSSStyleDeclaration) SetTextTransform(value string, important bool) { 601 | decl.Set("text-transform", value, important) 602 | } 603 | 604 | func (decl CSSStyleDeclaration) SetTop(value string, important bool) { 605 | decl.Set("top", value, important) 606 | } 607 | 608 | func (decl CSSStyleDeclaration) SetVerticalAlign(value string, important bool) { 609 | decl.Set("vertical-align", value, important) 610 | } 611 | 612 | func (decl CSSStyleDeclaration) SetVisibility(value string, important bool) { 613 | decl.Set("visibility", value, important) 614 | } 615 | 616 | func (decl CSSStyleDeclaration) SetWidth(value string, important bool) { 617 | decl.Set("width", value, important) 618 | } 619 | 620 | func (decl CSSStyleDeclaration) SetZIndex(value string, important bool) { 621 | decl.Set("z-index", value, important) 622 | } 623 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | See [gweb.orsinium.dev](https://gweb.orsinium.dev/). 4 | 5 | ## Running locally 6 | 7 | Run an example: 8 | 9 | ```bash 10 | ./run.sh hello 11 | ``` 12 | 13 | It will serve example [hello](./hello/) on [localhost:1337](http://localhost:1337/) 14 | 15 | ## Build 16 | 17 | Build one: 18 | 19 | ```bash 20 | ./build.sh hello 21 | ``` 22 | 23 | Build all: 24 | 25 | ```bash 26 | python3 -m pip install -r requirements.txt 27 | python3 build_all.py 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/ball/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/life4/gweb/canvas" 9 | "github.com/life4/gweb/web" 10 | ) 11 | 12 | const BGColor = "#ecf0f1" 13 | const BallColor = "#2c3e50" 14 | const TextColor = "#2c3e50" 15 | 16 | type Ball struct { 17 | context canvas.Context2D 18 | size int 19 | // position 20 | x, y int 21 | // movement 22 | vectorX int 23 | vectorY int 24 | // borders 25 | windowWidth int 26 | windowHeight int 27 | color string 28 | } 29 | 30 | func (ctx *Ball) changeDirection() { 31 | // bounce from text box (where we draw FPS and score) 32 | if ctx.x+ctx.vectorX < 110+ctx.size && ctx.y+ctx.vectorY < 60 { 33 | ctx.vectorX = -ctx.vectorX 34 | } 35 | if ctx.x+ctx.vectorX < 110 && ctx.y+ctx.vectorY < 60+ctx.size { 36 | ctx.vectorY = -ctx.vectorY 37 | } 38 | 39 | // right and left 40 | if ctx.x+ctx.vectorX > ctx.windowWidth-ctx.size { 41 | ctx.vectorX = -ctx.vectorX 42 | } else if ctx.x+ctx.vectorX < ctx.size { 43 | ctx.vectorX = -ctx.vectorX 44 | } 45 | 46 | // bottom and top 47 | if ctx.y+ctx.vectorY > ctx.windowHeight-ctx.size { 48 | ctx.vectorY = -ctx.vectorY 49 | } else if ctx.y+ctx.vectorY < ctx.size { 50 | ctx.vectorY = -ctx.vectorY 51 | } 52 | } 53 | 54 | func (ctx *Ball) handle() { 55 | ctx.changeDirection() 56 | 57 | // clear out previous render 58 | ctx.context.SetFillStyle(BGColor) 59 | ctx.context.Rectangle(ctx.x-ctx.size, ctx.y-ctx.size, ctx.size*2, ctx.size*2).Filled().Draw() 60 | 61 | // move the ball 62 | ctx.x += ctx.vectorX 63 | ctx.y += ctx.vectorY 64 | 65 | // draw the ball 66 | ctx.context.SetFillStyle(ctx.color) 67 | ctx.context.BeginPath() 68 | ctx.context.Arc(ctx.x, ctx.y, ctx.size, 0, math.Pi*2) 69 | ctx.context.Fill() 70 | ctx.context.ClosePath() 71 | } 72 | 73 | type FPS struct { 74 | context canvas.Context2D 75 | updated time.Time 76 | } 77 | 78 | func (h *FPS) drawFPS(now time.Time) { 79 | // calculate FPS 80 | fps := time.Second / now.Sub(h.updated) 81 | text := fmt.Sprintf("%d FPS", int64(fps)) 82 | 83 | // clear 84 | h.context.SetFillStyle(BGColor) 85 | h.context.Rectangle(10, 10, 100, 20).Filled().Draw() 86 | 87 | // write 88 | h.context.Text().SetFont("bold 20px Roboto") 89 | h.context.SetFillStyle(TextColor) 90 | h.context.Text().Fill(text, 10, 30, 100) 91 | } 92 | 93 | func (h *FPS) handle() { 94 | now := time.Now() 95 | // update FPS counter every second 96 | if h.updated.Second() != now.Second() { 97 | h.drawFPS(now) 98 | } 99 | h.updated = now 100 | } 101 | 102 | type Click struct { 103 | context canvas.Context2D 104 | ball *Ball 105 | score int 106 | } 107 | 108 | func (ctx *Click) touched() { 109 | ctx.score += 1 110 | 111 | // speed up 112 | if ctx.ball.vectorX > 0 { 113 | ctx.ball.vectorX += 1 114 | } else { 115 | ctx.ball.vectorX -= 1 116 | } 117 | if ctx.ball.vectorY > 0 { 118 | ctx.ball.vectorY += 1 119 | } else { 120 | ctx.ball.vectorY -= 1 121 | } 122 | 123 | // change direction 124 | ctx.ball.vectorX = -ctx.ball.vectorX 125 | ctx.ball.vectorY = -ctx.ball.vectorY 126 | 127 | // make text 128 | var text string 129 | if ctx.score == 1 { 130 | text = fmt.Sprintf("%d hit", ctx.score) 131 | } else { 132 | text = fmt.Sprintf("%d hits", ctx.score) 133 | } 134 | 135 | // clear place where previous score was 136 | ctx.context.SetFillStyle(BGColor) 137 | ctx.context.Rectangle(10, 40, 100, 20).Filled().Draw() 138 | 139 | // draw the score 140 | ctx.context.SetFillStyle(TextColor) 141 | ctx.context.Text().SetFont("bold 20px Roboto") 142 | ctx.context.Text().Fill(text, 10, 60, 100) 143 | 144 | // change ball color 145 | var color string 146 | switch ctx.score % 6 { 147 | case 0: 148 | color = "#16a085" 149 | case 1: 150 | color = "#c0392b" 151 | case 2: 152 | color = "#8e44ad" 153 | case 3: 154 | color = "#27ae60" 155 | case 4: 156 | color = "#34495e" 157 | case 5: 158 | color = "#d35400" 159 | } 160 | ctx.ball.color = color 161 | } 162 | 163 | func (ctx *Click) handle(event web.Event) { 164 | mouseX := event.Get("clientX").Int() 165 | mouseY := event.Get("clientY").Int() 166 | 167 | hypotenuse := math.Pow(float64(ctx.ball.size+15), 2) 168 | cathetus1 := math.Pow(float64(mouseX-ctx.ball.x), 2) 169 | cathetus2 := math.Pow(float64(mouseY-ctx.ball.y), 2) 170 | if cathetus1+cathetus2 < hypotenuse { 171 | go ctx.touched() 172 | } 173 | } 174 | 175 | func main() { 176 | window := web.GetWindow() 177 | doc := window.Document() 178 | doc.SetTitle("Bouncing ball") 179 | body := doc.Body() 180 | 181 | // create canvas 182 | h := window.InnerHeight() - 40 183 | w := window.InnerWidth() - 40 184 | canvas := doc.CreateCanvas() 185 | canvas.SetHeight(h) 186 | canvas.SetWidth(w) 187 | body.Node().AppendChild(canvas.Node()) 188 | 189 | context := canvas.Context2D() 190 | 191 | // draw background 192 | context.SetFillStyle(BGColor) 193 | context.BeginPath() 194 | context.Rectangle(0, 0, w, h).Filled().Draw() 195 | context.Fill() 196 | context.ClosePath() 197 | 198 | // register animation handlers 199 | ball := Ball{ 200 | context: context, 201 | vectorX: 4, vectorY: -4, 202 | size: 35, x: 120, y: 120, 203 | windowWidth: w, windowHeight: h, 204 | color: BallColor, 205 | } 206 | fps := FPS{context: context, updated: time.Now()} 207 | handler := func() { 208 | ball.handle() 209 | fps.handle() 210 | } 211 | window.RequestAnimationFrame(handler, true) 212 | 213 | // register action handlers 214 | click := Click{context: context, ball: &ball, score: 0} 215 | canvas.EventTarget().Listen(web.EventTypeMouseDown, click.handle) 216 | 217 | // prevent ending of the program 218 | select {} 219 | } 220 | -------------------------------------------------------------------------------- /examples/bootstrap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/life4/gweb/web" 7 | ) 8 | 9 | type Listener struct { 10 | sync.WaitGroup 11 | } 12 | 13 | func (listener *Listener) showAlert(event web.Event) { 14 | window := web.GetWindow() 15 | doc := window.Document() 16 | 17 | // create alert 18 | div := doc.CreateElement("div") 19 | div.SetText("It works!") 20 | div.Class().Append("alert", "alert-success") 21 | div.Set("role", "alert") 22 | 23 | // add the element into 24 | body := doc.Body() 25 | body.Node().AppendChild(div.Node()) 26 | 27 | // allow to close the program (unblock `listener.Wait()`) 28 | listener.Done() 29 | } 30 | 31 | func main() { 32 | window := web.GetWindow() 33 | doc := window.Document() 34 | doc.SetTitle("Twitter bootstrap including example") 35 | 36 | // make 37 | link := doc.CreateElement("link") 38 | // since we have to set element-specific fields, we have to go in syscall/js style 39 | link.Set("rel", "stylesheet") 40 | link.Set("href", "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css") 41 | 42 | // register listener for load to show message only when CSS is ready 43 | listener := Listener{} 44 | listener.Add(1) 45 | link.EventTarget().Listen(web.EventTypeLoad, listener.showAlert) 46 | 47 | // add into 48 | head := doc.Head() 49 | head.Node().AppendChild(link.Node()) 50 | 51 | // wait for listener to end before closing the program 52 | listener.Wait() 53 | } 54 | -------------------------------------------------------------------------------- /examples/breakout/ball.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/life4/gweb/canvas" 7 | ) 8 | 9 | type Ball struct { 10 | Circle 11 | vector Vector 12 | 13 | windowWidth int 14 | windowHeight int 15 | 16 | context canvas.Context2D 17 | platform *Platform 18 | } 19 | 20 | func (ball *Ball) BounceFromPoint(point Point) { 21 | // ball.context.SetFillStyle("red") 22 | // ball.context.Rectangle(point.x, point.y, 2, 2).Filled().Draw() 23 | 24 | normal := Vector{ 25 | x: float64(point.x - ball.x), 26 | y: float64(point.y - ball.y), 27 | } 28 | normal = normal.Normalized() 29 | dot := ball.vector.Dot(normal) 30 | ball.vector = ball.vector.Sub(normal.Mul(2 * dot)) 31 | } 32 | 33 | func (ball *Ball) changeDirection() { 34 | // bounce from text box (where we draw FPS and score) 35 | // bounce from right border of the text box 36 | if ball.x-ball.radius <= TextRight && ball.y < TextBottom+10 { 37 | ball.vector.x = -ball.vector.x 38 | } 39 | // bounce from bottom of the text box 40 | if ball.x <= TextRight && ball.y-ball.radius < TextBottom+10 { 41 | ball.vector.y = -ball.vector.y 42 | } 43 | 44 | // right and left of the playground 45 | if ball.vector.x > 0 && ball.x > ball.windowWidth-ball.radius { 46 | ball.vector.x = -ball.vector.x 47 | } 48 | if ball.vector.x < 0 && ball.x < ball.radius { 49 | ball.vector.x = -ball.vector.x 50 | } 51 | 52 | // bottom and top of the playground 53 | // if ball.vector.y > 0 && ball.y+ball.radius >= ball.windowHeight { 54 | // ball.vector.y = -ball.vector.y 55 | // } 56 | if ball.vector.y < 0 && ball.y-ball.radius <= 0 { 57 | ball.vector.y = -ball.vector.y 58 | } 59 | 60 | // bounce from platform edges 61 | point := ball.platform.Touch(*ball) 62 | if point != nil { 63 | ball.BounceFromPoint(*point) 64 | } 65 | } 66 | 67 | func (ball *Ball) handle() { 68 | // clear out previous render 69 | ball.context.SetFillStyle(BGColor) 70 | ball.context.BeginPath() 71 | ball.context.Arc(ball.x, ball.y, ball.radius+1, 0, math.Pi*2) 72 | ball.context.Fill() 73 | ball.context.ClosePath() 74 | 75 | ball.changeDirection() 76 | 77 | // move the ball 78 | ball.x += int(math.Round(ball.vector.x)) 79 | ball.y += int(math.Round(ball.vector.y)) 80 | 81 | // draw the ball 82 | ball.context.SetFillStyle(BallColor) 83 | ball.context.BeginPath() 84 | ball.context.Arc(ball.x, ball.y, ball.radius, 0, math.Pi*2) 85 | ball.context.Fill() 86 | ball.context.ClosePath() 87 | } 88 | -------------------------------------------------------------------------------- /examples/breakout/ball_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBounceFromPoint(t *testing.T) { 10 | ball := Ball{ 11 | Circle: Circle{x: 10, y: 10, radius: 20}, 12 | vector: Vector{x: 5, y: 0}, 13 | } 14 | 15 | // bounce from the right 16 | ball.BounceFromPoint(Point{x: 30, y: 10}) 17 | assert.InDelta(t, ball.vector.x, -5, 0.0001, "bounce from the right: x") 18 | assert.InDelta(t, ball.vector.y, 0, 0.0001, "bounce from the right: y") 19 | 20 | // bounce from the left 21 | ball.vector = Vector{x: -5, y: 0} 22 | ball.BounceFromPoint(Point{x: -10, y: 10}) 23 | assert.InDelta(t, ball.vector.x, 5, 0.0001, "bounce from the left: x") 24 | assert.InDelta(t, ball.vector.y, 0, 0.0001, "bounce from the left: y") 25 | 26 | // bounce from the bottom 27 | ball.vector = Vector{x: 0, y: 5} 28 | ball.BounceFromPoint(Point{x: 10, y: 30}) 29 | assert.InDelta(t, ball.vector.x, 0, 0.0001, "bounce from the bottom: x") 30 | assert.InDelta(t, ball.vector.y, -5, 0.0001, "bounce from the bottom: y") 31 | } 32 | -------------------------------------------------------------------------------- /examples/breakout/brick.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/canvas" 4 | 5 | type Brick struct { 6 | Rectangle 7 | context canvas.Context2D 8 | cost int 9 | removed bool 10 | } 11 | 12 | func (brick *Brick) Collide(ball *Ball, bounce bool) bool { 13 | if brick.removed { 14 | return false 15 | } 16 | 17 | // quick checks of ball position 18 | if ball.x-ball.radius > brick.x+brick.width { // ball righter 19 | return false 20 | } 21 | if ball.x+ball.radius < brick.x { // ball lefter 22 | return false 23 | } 24 | if ball.y+ball.radius < brick.y { // ball upper 25 | return false 26 | } 27 | if ball.y-ball.radius > brick.y+brick.height { // ball downer 28 | return false 29 | } 30 | 31 | points := [...]Point{ 32 | // bottom of brick collision 33 | {x: ball.x, y: ball.y - ball.radius}, 34 | // top of brick collision 35 | {x: ball.x + brick.width, y: ball.y + ball.radius}, 36 | // left of brick collision 37 | {x: ball.x, y: ball.y + ball.radius}, 38 | // right of brick collision 39 | {x: ball.x + brick.width, y: ball.y - ball.radius}, 40 | } 41 | 42 | for _, point := range points { 43 | if brick.Contains(point) { 44 | if bounce { 45 | ball.BounceFromPoint(point) 46 | } 47 | return true 48 | } 49 | } 50 | 51 | points = [...]Point{ 52 | // left-top corner of the brick 53 | {x: brick.x, y: brick.y}, 54 | // right-top corner of the brick 55 | {x: brick.x + brick.width, y: brick.y}, 56 | // left-bottom corner of the brick 57 | {x: brick.x, y: brick.y + brick.height}, 58 | // right-bottom corner of the brick 59 | {x: brick.x + brick.width, y: brick.y + brick.height}, 60 | } 61 | 62 | for _, point := range points { 63 | if ball.Contains(point) { 64 | if bounce { 65 | ball.BounceFromPoint(point) 66 | } 67 | return true 68 | } 69 | } 70 | 71 | return false 72 | } 73 | 74 | func (brick *Brick) Draw(color string) { 75 | brick.context.SetFillStyle(color) 76 | brick.context.Rectangle(brick.x, brick.y, brick.width, brick.height).Filled().Draw() 77 | brick.removed = false 78 | } 79 | 80 | func (brick *Brick) Remove() { 81 | brick.context.SetFillStyle(BGColor) 82 | brick.context.Rectangle(brick.x, brick.y, brick.width, brick.height).Filled().Draw() 83 | brick.removed = true 84 | } 85 | -------------------------------------------------------------------------------- /examples/breakout/bricks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/life4/gweb/canvas" 5 | ) 6 | 7 | type Bricks struct { 8 | context canvas.Context2D 9 | registry []*Brick 10 | ready bool 11 | windowWidth int 12 | windowHeight int 13 | 14 | // stat 15 | score int 16 | hits int 17 | text *TextBlock 18 | } 19 | 20 | func (bricks *Bricks) Draw() { 21 | bricks.registry = make([]*Brick, BrickCols*BrickRows) 22 | width := (bricks.windowWidth-BrickMarginLeft)/BrickCols - BrickMarginX 23 | colors := [...]string{"#c0392b", "#d35400", "#f39c12", "#f1c40f"} 24 | costs := [...]int{7, 5, 3, 1} 25 | for i := 0; i < BrickCols; i++ { 26 | for j := 0; j < BrickRows; j++ { 27 | x := BrickMarginLeft + (width+BrickMarginX)*i 28 | y := BrickMarginTop + (BrickHeight+BrickMarginY)*j 29 | color := colors[(j/2)%len(colors)] 30 | cost := costs[(j/2)%len(colors)] 31 | 32 | brick := Brick{ 33 | context: bricks.context, 34 | Rectangle: Rectangle{x: x, y: y, width: width, height: BrickHeight}, 35 | cost: cost, 36 | } 37 | brick.Draw(color) 38 | bricks.registry[BrickRows*i+j] = &brick 39 | } 40 | } 41 | bricks.ready = true 42 | } 43 | 44 | func (bricks *Bricks) Handle(ball *Ball) { 45 | if !bricks.ready { 46 | return 47 | } 48 | changed := false 49 | for _, brick := range bricks.registry { 50 | // we bounce the ball only on first collision with a brick in a frame 51 | if !brick.Collide(ball, !changed) { 52 | continue 53 | } 54 | // if the ball touched the brick, remove the brick and count score 55 | brick.Remove() 56 | bricks.score += brick.cost 57 | bricks.hits += 1 58 | changed = true 59 | } 60 | if changed { 61 | // re-draw stat 62 | go bricks.text.DrawScore(bricks.score) 63 | go bricks.text.DrawHits(bricks.hits) 64 | 65 | // speed up ball after some hits 66 | speedUpHits := [...]int{4, 8, 16, 24, 32, 64} 67 | for _, hits := range speedUpHits { 68 | if bricks.hits == hits { 69 | ball.vector.x += sign(ball.vector.x) * 1 70 | ball.vector.y += sign(ball.vector.y) * 1 71 | break 72 | } 73 | } 74 | } 75 | } 76 | 77 | func (bricks *Bricks) Count() int { 78 | count := 0 79 | for _, brick := range bricks.registry { 80 | if !brick.removed { 81 | count += 1 82 | } 83 | } 84 | return count 85 | } 86 | 87 | func sign(n float64) float64 { 88 | if n >= 0 { 89 | return 1 90 | } 91 | return -1 92 | } 93 | -------------------------------------------------------------------------------- /examples/breakout/game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/life4/gweb/web" 9 | ) 10 | 11 | type Game struct { 12 | Width int 13 | Height int 14 | Window web.Window 15 | Canvas web.Canvas 16 | Body web.HTMLElement 17 | 18 | state *State 19 | platform Platform 20 | ball Ball 21 | block TextBlock 22 | bricks Bricks 23 | } 24 | 25 | func (game *Game) Init() { 26 | game.state = &State{Stop: SubState{}} 27 | context := game.Canvas.Context2D() 28 | 29 | // draw background 30 | context.SetFillStyle(BGColor) 31 | context.BeginPath() 32 | context.Rectangle(0, 0, game.Width, game.Height).Filled().Draw() 33 | context.Fill() 34 | context.ClosePath() 35 | 36 | // make handlers 37 | rect := Rectangle{ 38 | x: game.Width / 2, 39 | y: game.Height - 60, 40 | width: PlatformWidth, 41 | height: PlatformHeight, 42 | } 43 | platformCicrle := CircleFromRectangle(rect) 44 | game.platform = Platform{ 45 | rect: &rect, 46 | circle: &platformCicrle, 47 | context: context, 48 | element: game.Canvas, 49 | mouseX: game.Width / 2, 50 | windowWidth: game.Width, 51 | windowHeight: game.Height, 52 | } 53 | game.block = TextBlock{context: context, updated: time.Now()} 54 | ballCircle := Circle{ 55 | x: game.platform.circle.x, 56 | y: game.platform.rect.y - BallSize - 5, 57 | radius: BallSize, 58 | } 59 | game.ball = Ball{ 60 | context: context, 61 | vector: Vector{x: 5, y: -5}, 62 | Circle: ballCircle, 63 | windowWidth: game.Width, 64 | windowHeight: game.Height, 65 | platform: &game.platform, 66 | } 67 | game.bricks = Bricks{ 68 | context: context, 69 | windowWidth: game.Width, 70 | windowHeight: game.Height, 71 | ready: false, 72 | text: &game.block, 73 | } 74 | go game.bricks.Draw() 75 | } 76 | 77 | func (game *Game) handler() { 78 | if game.state.Stop.Requested { 79 | game.state.Stop.Complete() 80 | return 81 | } 82 | 83 | wg := sync.WaitGroup{} 84 | wg.Add(5) 85 | go func() { 86 | // update FPS 87 | game.block.handle() 88 | wg.Done() 89 | }() 90 | go func() { 91 | // update platform position 92 | game.platform.handleFrame() 93 | wg.Done() 94 | }() 95 | go func() { 96 | // check if the ball should bounce from a brick 97 | game.bricks.Handle(&game.ball) 98 | wg.Done() 99 | }() 100 | go func() { 101 | // check if the ball should bounce from border or platform 102 | game.ball.handle() 103 | wg.Done() 104 | }() 105 | go func() { 106 | // check if ball got out of playground 107 | if game.ball.y >= game.Height { 108 | go game.fail() 109 | } 110 | if game.bricks.Count() == 0 { 111 | go game.win() 112 | } 113 | wg.Done() 114 | }() 115 | wg.Wait() 116 | 117 | game.Window.RequestAnimationFrame(game.handler, false) 118 | } 119 | 120 | func (game *Game) Register() { 121 | game.state = &State{Stop: SubState{}} 122 | // register mouse movement handler 123 | game.Body.EventTarget().Listen(web.EventTypeMouseMove, game.platform.handleMouse) 124 | // register frame updaters 125 | game.Window.RequestAnimationFrame(game.handler, false) 126 | } 127 | 128 | func (game *Game) Stop() { 129 | if game.state.Stop.Completed { 130 | return 131 | } 132 | game.state.Stop.Request() 133 | game.state.Stop.Wait() 134 | } 135 | 136 | func (game *Game) fail() { 137 | game.Stop() 138 | game.drawText("Game Over", FailColor) 139 | } 140 | 141 | func (game *Game) win() { 142 | game.Stop() 143 | game.drawText("You Win", WinColor) 144 | } 145 | 146 | func (game *Game) drawText(text, color string) { 147 | height := TextHeight * 2 148 | width := TextWidth * 2 149 | context := game.Canvas.Context2D() 150 | context.Text().SetFont(fmt.Sprintf("bold %dpx Roboto", height)) 151 | context.SetFillStyle(color) 152 | context.Text().Fill(text, (game.Width-width)/2, (game.Height-height)/2, width) 153 | } 154 | -------------------------------------------------------------------------------- /examples/breakout/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/life4/gweb/web" 5 | ) 6 | 7 | func main() { 8 | window := web.GetWindow() 9 | doc := window.Document() 10 | doc.SetTitle("Breakout") 11 | body := doc.Body() 12 | 13 | // create canvas 14 | h := window.InnerHeight() - 50 15 | w := window.InnerWidth() - 40 16 | canvas := doc.CreateCanvas() 17 | canvas.SetHeight(h) 18 | canvas.SetWidth(w) 19 | body.Node().AppendChild(canvas.Node()) 20 | 21 | game := Game{ 22 | Width: w, 23 | Height: h, 24 | Window: window, 25 | Canvas: canvas, 26 | Body: body, 27 | } 28 | game.Init() 29 | game.Register() 30 | 31 | restartButton := doc.CreateElement("button") 32 | restartButton.SetText("restart") 33 | restartHandler := func(event web.Event) { 34 | go func() { 35 | game.Stop() 36 | game.Init() 37 | game.Register() 38 | }() 39 | } 40 | restartButton.EventTarget().Listen(web.EventTypeMouseDown, restartHandler) 41 | body.Node().AppendChild(restartButton.Node()) 42 | 43 | pauseButton := doc.CreateElement("button") 44 | pauseButton.SetText("pause") 45 | pauseHandler := func(event web.Event) { 46 | go func() { 47 | if !game.state.Stop.Requested { 48 | game.Stop() 49 | pauseButton.SetText("play") 50 | } else { 51 | game.Register() 52 | pauseButton.SetText("pause") 53 | } 54 | }() 55 | } 56 | pauseButton.Style().SetMargin("0px 5px", false) 57 | pauseButton.EventTarget().Listen(web.EventTypeMouseDown, pauseHandler) 58 | body.Node().AppendChild(pauseButton.Node()) 59 | 60 | sourceLink := doc.CreateElement("a") 61 | sourceLink.SetText("source") 62 | sourceLink.Set("href", "https://github.com/life4/gweb/tree/master/examples/breakout") 63 | sourceLink.Set("target", "_blank") 64 | body.Node().AppendChild(sourceLink.Node()) 65 | 66 | // prevent ending of the program 67 | select {} 68 | } 69 | -------------------------------------------------------------------------------- /examples/breakout/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestVectorRotate(t *testing.T) { 11 | f := func(gx, gy float64, angle float64, ex, ey float64) { 12 | v := Vector{x: gx, y: gy} 13 | actual := v.Rotate(angle) 14 | assert.InDelta(t, actual.x, ex, 0.0001) 15 | assert.InDelta(t, actual.y, ey, 0.0001) 16 | } 17 | f(10, 10, math.Pi, -10, -10) 18 | f(10, 10, 2*math.Pi, 10, 10) 19 | f(10, 10, math.Pi/2, -10, 10) 20 | f(10, 10, math.Pi*3/2, 10, -10) 21 | } 22 | 23 | func TestCircleFromRectangleRadius(t *testing.T) { 24 | f := func(w, h, expected int) { 25 | circle := CircleFromRectangle(Rectangle{ 26 | width: w, 27 | height: h, 28 | }) 29 | if expected != 0 { 30 | assert.Equal(t, circle.radius, expected) 31 | } 32 | assert.GreaterOrEqual(t, circle.radius, w/2) 33 | } 34 | 35 | f(10, 5, 5) 36 | f(80, 30, 41) 37 | f(10, 5, 0) 38 | f(5, 10, 0) 39 | } 40 | -------------------------------------------------------------------------------- /examples/breakout/platform.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/life4/gweb/canvas" 7 | "github.com/life4/gweb/web" 8 | ) 9 | 10 | type Platform struct { 11 | circle *Circle 12 | rect *Rectangle 13 | 14 | context canvas.Context2D 15 | element web.Canvas 16 | // movement 17 | mouseX int 18 | // borders 19 | windowWidth int 20 | windowHeight int 21 | } 22 | 23 | func (pl Platform) Contains(point Point) bool { 24 | return pl.circle.Contains(point) && pl.rect.Contains(point) 25 | } 26 | 27 | // Touch returns touch point of platform and ball if any 28 | func (pl Platform) Touch(ball Ball) *Point { 29 | point := pl.touchInside(ball) 30 | if point != nil { 31 | return point 32 | } 33 | point = pl.touchUp(ball) 34 | if point != nil { 35 | return point 36 | } 37 | return pl.touchCorners(ball) 38 | } 39 | 40 | func (pl Platform) touchInside(ball Ball) *Point { 41 | // don't bounce if ball moves up 42 | if ball.vector.y < -1.0 { 43 | return nil 44 | } 45 | 46 | point := &Point{x: ball.x, y: ball.y} 47 | if pl.Contains(*point) { 48 | point.y = ball.y + ball.radius 49 | return point 50 | } 51 | return nil 52 | } 53 | 54 | func (pl Platform) touchUp(ball Ball) *Point { 55 | // don't bounce if ball is inside of the platform 56 | if ball.y > pl.circle.y { 57 | return nil 58 | } 59 | // don't bounce if ball moves up 60 | if ball.vector.y < -1.0 { 61 | return nil 62 | } 63 | 64 | catx := float64(ball.x - pl.circle.x) 65 | caty := float64(ball.y - pl.circle.y) 66 | 67 | // check if ball is too far from platform circle 68 | hypotenuse := math.Sqrt(math.Pow(catx, 2) + math.Pow(caty, 2)) 69 | distance := math.Abs(float64(ball.radius + pl.circle.radius)) 70 | if hypotenuse > distance+PlatformAura { 71 | return nil 72 | } 73 | 74 | ratio := float64(ball.radius) / float64(pl.circle.radius) 75 | point := Point{ 76 | x: ball.x - int(catx*ratio), 77 | y: ball.y - int(caty*ratio), 78 | } 79 | 80 | // check if ball is lower than platform low line 81 | if point.y >= pl.rect.y+pl.rect.height { 82 | return nil 83 | } 84 | 85 | return &point 86 | } 87 | 88 | func (pl Platform) touchCorners(ball Ball) *Point { 89 | // left 90 | if ball.vector.x > 0 { 91 | point := Point{ 92 | x: pl.rect.x, 93 | y: pl.rect.y + pl.rect.height, 94 | } 95 | if ball.Contains(point) { 96 | return &point 97 | } 98 | } 99 | 100 | // right 101 | if ball.vector.x < 0 { 102 | point := Point{ 103 | x: pl.rect.x + pl.rect.width, 104 | y: pl.rect.y + pl.rect.height, 105 | } 106 | if ball.Contains(point) { 107 | return &point 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | func (pl Platform) angle() float64 { 114 | tan := float64(pl.rect.width/2) / float64(pl.circle.radius-pl.rect.height) 115 | return math.Atan(tan) 116 | } 117 | 118 | func (ctx *Platform) changePosition() { 119 | path := ctx.mouseX - (ctx.rect.x + ctx.rect.width/2) 120 | if path == 0 { 121 | return 122 | } 123 | 124 | // don't move too fast 125 | if path > 0 && path > PlatformMaxSpeed { 126 | path = PlatformMaxSpeed 127 | } else if path < 0 && path < -PlatformMaxSpeed { 128 | path = -PlatformMaxSpeed 129 | } 130 | 131 | // don't move out of playground 132 | if ctx.rect.x+path <= 0 { 133 | ctx.rect.x = 0 134 | return 135 | } 136 | if ctx.rect.x+path >= ctx.windowWidth-ctx.rect.width { 137 | ctx.rect.x = ctx.windowWidth - ctx.rect.width 138 | return 139 | } 140 | 141 | ctx.rect.x += path 142 | ctx.circle.x = ctx.rect.x + ctx.rect.width/2 143 | } 144 | 145 | func (platform *Platform) handleMouse(event web.Event) { 146 | platform.mouseX = event.Get("clientX").Int() 147 | } 148 | 149 | func (ctx *Platform) handleFrame() { 150 | // clear out previous render 151 | ctx.draw(BGColor, 1) 152 | 153 | // change platform coordinates 154 | ctx.changePosition() 155 | 156 | // draw the platform 157 | ctx.draw(PlatformColor, 0) 158 | } 159 | 160 | func (pl Platform) draw(color string, delta int) { 161 | // pl.context.SetFillStyle("red") 162 | // pl.context.Rectangle(pl.circle.x, pl.rect.y+pl.circle.radius, 2, 2).Filled().Draw() 163 | // pl.context.SetFillStyle("green") 164 | // pl.context.Rectangle(pl.circle.x, pl.circle.y, 2, 2).Filled().Draw() 165 | 166 | pl.context.SetFillStyle(color) 167 | pl.context.BeginPath() 168 | pl.context.Arc( 169 | pl.circle.x, pl.circle.y, 170 | pl.circle.radius+delta, 171 | 1.5*math.Pi-pl.angle()-(float64(delta)/math.Pi/2), 172 | 1.5*math.Pi+pl.angle()+(float64(delta)/math.Pi/2), 173 | ) 174 | pl.context.Fill() 175 | pl.context.ClosePath() 176 | } 177 | -------------------------------------------------------------------------------- /examples/breakout/settings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // colors 4 | const ( 5 | BGColor = "#ecf0f1" 6 | BallColor = "#27ae60" 7 | PlatformColor = "#2c3e50" 8 | TextColor = PlatformColor 9 | FailColor = "#c0392b" 10 | WinColor = BallColor 11 | ) 12 | 13 | // platform 14 | const ( 15 | PlatformWidth = 120 16 | PlatformHeight = 20 17 | PlatformMaxSpeed = 40 18 | PlatformAura = 5 // additional invisible bounce space around the platform 19 | ) 20 | 21 | // ball 22 | const BallSize = 20 23 | 24 | // bricks 25 | const ( 26 | BrickHeight = 20 27 | BrickRows = 8 28 | BrickCols = 14 29 | BrickMarginLeft = 120 // pixels 30 | BrickMarginTop = 10 // pixels 31 | BrickMarginX = 5 // pixels 32 | BrickMarginY = 5 // pixels 33 | ) 34 | 35 | // text box 36 | const ( 37 | TextWidth = 90 38 | TextHeight = 20 39 | TextLeft = 10 40 | TextTop = 10 41 | TextMargin = 5 42 | 43 | TextBottom = TextTop + (TextHeight+TextMargin)*3 44 | TextRight = TextLeft + TextWidth 45 | ) 46 | -------------------------------------------------------------------------------- /examples/breakout/shapes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "math" 4 | 5 | type Point struct{ x, y int } 6 | 7 | type Rectangle struct{ x, y, width, height int } 8 | 9 | func (rectangle Rectangle) Contains(point Point) bool { 10 | if point.y < rectangle.y { // point upper 11 | return false 12 | } 13 | if point.y > rectangle.y+rectangle.height { // point downer 14 | return false 15 | } 16 | if point.x > rectangle.x+rectangle.width { // point righter 17 | return false 18 | } 19 | if point.x < rectangle.x { // point lefter 20 | return false 21 | } 22 | return true 23 | 24 | } 25 | 26 | type Circle struct{ x, y, radius int } 27 | 28 | func (circle Circle) Contains(point Point) bool { 29 | hypotenuse := math.Pow(float64(circle.radius), 2) 30 | cathetus1 := math.Pow(float64(point.x-circle.x), 2) 31 | cathetus2 := math.Pow(float64(point.y-circle.y), 2) 32 | return cathetus1+cathetus2 < hypotenuse 33 | } 34 | 35 | func CircleFromRectangle(rect Rectangle) Circle { 36 | base := math.Sqrt(math.Pow(float64(rect.width)/2, 2) + math.Pow(float64(rect.height), 2)) 37 | cos := float64(rect.height) / base 38 | radius := int(base / 2 / cos) 39 | 40 | return Circle{ 41 | x: rect.x + rect.width/2, 42 | y: rect.y + radius, 43 | radius: radius, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/breakout/state.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type State struct { 8 | Stop SubState 9 | } 10 | 11 | type SubState struct { 12 | Requested bool 13 | Completed bool 14 | wg sync.WaitGroup 15 | } 16 | 17 | func (state *SubState) Request() { 18 | state.Requested = true 19 | state.Completed = false 20 | state.wg = sync.WaitGroup{} 21 | state.wg.Add(1) 22 | } 23 | 24 | func (state *SubState) Complete() { 25 | if !state.Requested { 26 | return 27 | } 28 | state.Completed = true 29 | state.wg.Done() 30 | } 31 | 32 | func (state *SubState) Wait() { 33 | state.wg.Wait() 34 | } 35 | -------------------------------------------------------------------------------- /examples/breakout/test.sh: -------------------------------------------------------------------------------- 1 | GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest . 2 | -------------------------------------------------------------------------------- /examples/breakout/text_block.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/life4/gweb/canvas" 8 | ) 9 | 10 | type TextBlock struct { 11 | context canvas.Context2D 12 | updated time.Time 13 | } 14 | 15 | func (block TextBlock) drawFPS(now time.Time) { 16 | // calculate FPS 17 | fps := time.Second / now.Sub(block.updated) 18 | text := fmt.Sprintf("%d FPS", int64(fps)) 19 | block.drawText(text, 0) 20 | } 21 | 22 | func (block *TextBlock) handle() { 23 | now := time.Now() 24 | // update FPS counter every second 25 | if block.updated.Second() != now.Second() { 26 | block.drawFPS(now) 27 | } 28 | block.updated = now 29 | } 30 | 31 | func (block TextBlock) drawText(text string, row int) { 32 | x := TextLeft 33 | y := TextTop + row*(TextMargin+TextHeight) 34 | 35 | // clear place where previous score was 36 | block.context.SetFillStyle(BGColor) 37 | block.context.Rectangle(x, y, TextWidth, TextHeight+TextMargin).Filled().Draw() 38 | 39 | // draw the text 40 | block.context.SetFillStyle(TextColor) 41 | block.context.Text().SetFont(fmt.Sprintf("bold %dpx Roboto", TextHeight)) 42 | block.context.Text().Fill(text, x, y+TextHeight, TextWidth) 43 | } 44 | 45 | func (block TextBlock) DrawScore(score int) { 46 | // make text 47 | var text string 48 | if score == 1 { 49 | text = fmt.Sprintf("%d point", score) 50 | } else { 51 | text = fmt.Sprintf("%d points", score) 52 | } 53 | block.drawText(text, 1) 54 | } 55 | 56 | func (block TextBlock) DrawHits(hits int) { 57 | // make text 58 | var text string 59 | if hits == 1 { 60 | text = fmt.Sprintf("%d hit", hits) 61 | } else { 62 | text = fmt.Sprintf("%d hits", hits) 63 | } 64 | block.drawText(text, 2) 65 | } 66 | -------------------------------------------------------------------------------- /examples/breakout/vector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "math" 4 | 5 | type Vector struct { 6 | x, y float64 7 | } 8 | 9 | func (vector *Vector) Rotate(angle float64) Vector { 10 | sin := math.Sin(angle) 11 | cos := math.Cos(angle) 12 | return Vector{ 13 | x: vector.x*cos - vector.y*sin, 14 | y: vector.x*sin + vector.y*cos, 15 | } 16 | } 17 | 18 | func (vector Vector) Len() float64 { 19 | return math.Sqrt(math.Pow(vector.x, 2) + math.Pow(vector.y, 2)) 20 | } 21 | 22 | func (vector Vector) Angle(other Vector) float64 { 23 | return vector.Dot(other) / (vector.Len() * other.Len()) 24 | } 25 | 26 | func (vector Vector) Dot(other Vector) float64 { 27 | return vector.x*other.x + vector.y*other.y 28 | } 29 | 30 | func (vector Vector) Sub(other Vector) Vector { 31 | return Vector{x: vector.x - other.x, y: vector.y - other.y} 32 | } 33 | 34 | func (vector Vector) Mul(value float64) Vector { 35 | return Vector{x: vector.x * value, y: vector.y * value} 36 | } 37 | 38 | func (vector Vector) Normalized() Vector { 39 | value := 1.0 / vector.Len() 40 | return vector.Mul(value) 41 | } 42 | -------------------------------------------------------------------------------- /examples/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | mkdir -p $SCRIPT_DIR/build 5 | cp $SCRIPT_DIR/frontend/* $SCRIPT_DIR/build/ 6 | cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" $SCRIPT_DIR/build/script.js 7 | GOOS=js GOARCH=wasm go build -o $SCRIPT_DIR/build/frontend.wasm $SCRIPT_DIR/$1/ 8 | -------------------------------------------------------------------------------- /examples/build_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | from argparse import ArgumentParser 4 | from pathlib import Path 5 | from shutil import copytree, rmtree 6 | 7 | import yaml 8 | from jinja2 import Environment, FileSystemLoader 9 | 10 | 11 | ROOT = Path(__file__).absolute().parent 12 | env = Environment( 13 | loader=FileSystemLoader(ROOT), 14 | extensions=['jinja2_markdown.MarkdownExtension'], 15 | ) 16 | 17 | parser = ArgumentParser() 18 | parser.add_argument( 19 | '-o', '--output', 20 | default=str(ROOT.parent / 'public'), 21 | help='path to build output', 22 | ) 23 | 24 | 25 | def make_index(): 26 | with (ROOT / 'index.yml').open(encoding='utf8') as stream: 27 | data = yaml.safe_load(stream) 28 | template = env.get_template('index.html.j2') 29 | return template.render(**data) 30 | 31 | 32 | def get_examples(): 33 | for path in ROOT.iterdir(): 34 | if not path.is_dir(): 35 | continue 36 | if not (path / 'main.go').exists(): 37 | continue 38 | if path.name in ('server', 'build', 'frontend'): 39 | continue 40 | yield path 41 | 42 | 43 | def main(args) -> int: 44 | if args.output: 45 | build_path = Path(args.output).resolve() 46 | else: 47 | build_path = Path(__file__).absolute().parent.parent / 'build' 48 | build_path.mkdir(exist_ok=True) 49 | 50 | (build_path / 'index.html').write_text(make_index()) 51 | 52 | for path in get_examples(): 53 | cmd = [str(path.parent / 'build.sh'), path.name] 54 | result = subprocess.run(cmd) 55 | if result.returncode != 0: 56 | return 1 57 | src = path.parent / 'build' 58 | assert src.exists() 59 | dst = build_path / path.name 60 | if dst.exists(): 61 | rmtree(str(dst)) 62 | copytree(src=str(src), dst=str(dst)) 63 | 64 | print(build_path) 65 | return 0 66 | 67 | 68 | if __name__ == '__main__': 69 | args = parser.parse_args() 70 | exit(main(args)) 71 | -------------------------------------------------------------------------------- /examples/draw/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/life4/gweb/canvas" 9 | "github.com/life4/gweb/web" 10 | ) 11 | 12 | const BGColor = "#ecf0f1" 13 | const PointColor = "#2c3e50" 14 | const TextColor = "#2c3e50" 15 | 16 | type Handler struct { 17 | context canvas.Context2D 18 | drawing bool 19 | updated time.Time 20 | } 21 | 22 | func (h *Handler) handleStart(event web.Event) { 23 | h.drawing = true 24 | } 25 | 26 | func (h *Handler) handleEnd(event web.Event) { 27 | h.drawing = false 28 | } 29 | 30 | func (h *Handler) handleMove(event web.Event) { 31 | if !h.drawing { 32 | return 33 | } 34 | 35 | // draw a point 36 | x := event.Get("clientX").Int() 37 | y := event.Get("clientY").Int() 38 | h.context.SetFillStyle(PointColor) 39 | h.context.BeginPath() 40 | h.context.Arc(x, y, 10, 0, math.Pi*2) 41 | h.context.Fill() 42 | } 43 | 44 | func (h *Handler) handleFrame() { 45 | now := time.Now() 46 | // update FPS counter every second 47 | if h.updated.Second() != now.Second() { 48 | // calculate FPS 49 | fps := time.Second / now.Sub(h.updated) 50 | text := fmt.Sprintf("%d FPS", int64(fps)) 51 | 52 | // clear 53 | h.context.SetFillStyle(BGColor) 54 | h.context.Rectangle(10, 10, 100, 20).Filled().Draw() 55 | 56 | // write 57 | h.context.Text().SetFont("bold 20px Roboto") 58 | h.context.SetFillStyle(TextColor) 59 | h.context.Text().Fill(text, 10, 30, 100) 60 | } 61 | h.updated = now 62 | } 63 | 64 | func main() { 65 | window := web.GetWindow() 66 | doc := window.Document() 67 | doc.SetTitle("Canvas drawing example") 68 | body := doc.Body() 69 | 70 | // create canvas 71 | h := window.InnerHeight() - 40 72 | w := window.InnerWidth() - 40 73 | canvas := doc.CreateCanvas() 74 | canvas.SetHeight(h) 75 | canvas.SetWidth(w) 76 | body.Node().AppendChild(canvas.Node()) 77 | 78 | context := canvas.Context2D() 79 | 80 | // draw background 81 | context.SetFillStyle(BGColor) 82 | context.BeginPath() 83 | context.Rectangle(0, 0, w, h).Filled().Draw() 84 | context.Fill() 85 | context.ClosePath() 86 | 87 | // register handlers 88 | handler := Handler{context: context, drawing: false, updated: time.Now()} 89 | canvas.EventTarget().Listen(web.EventTypeMouseDown, handler.handleStart) 90 | canvas.EventTarget().Listen(web.EventTypeMouseUp, handler.handleEnd) 91 | canvas.EventTarget().Listen(web.EventTypeMouseMove, handler.handleMove) 92 | 93 | window.RequestAnimationFrame(handler.handleFrame, true) 94 | // prevent ending of the program 95 | select {} 96 | } 97 | -------------------------------------------------------------------------------- /examples/events/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/life4/gweb/web" 7 | ) 8 | 9 | func handleMouseMove(event web.Event) { 10 | x := event.Get("clientX").Int() 11 | y := event.Get("clientY").Int() 12 | 13 | element := event.CurrentTarget().HTMLElement() 14 | text := fmt.Sprintf("The mouse position is %d x %d", x, y) 15 | element.SetText(text) 16 | } 17 | 18 | func main() { 19 | window := web.GetWindow() 20 | doc := window.Document() 21 | doc.SetTitle("Events handling example") 22 | 23 | // create
24 | div := doc.CreateElement("div") 25 | div.SetText("no movement") 26 | 27 | // fill all the page by the element 28 | h := window.InnerHeight() 29 | w := window.InnerWidth() 30 | div.Style().SetHeight(fmt.Sprintf("%dpx", h), false) 31 | div.Style().SetWidth(fmt.Sprintf("%dpx", w), false) 32 | 33 | // register the listener 34 | div.EventTarget().Listen(web.EventTypeMouseMove, handleMouseMove) 35 | 36 | // add the element into 37 | body := doc.Body() 38 | body.Node().AppendChild(div.Node()) 39 | 40 | // prevent the script from stopping 41 | select {} 42 | } 43 | -------------------------------------------------------------------------------- /examples/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/frontend/loader.js: -------------------------------------------------------------------------------- 1 | if (!WebAssembly.instantiateStreaming) { // polyfill 2 | WebAssembly.instantiateStreaming = async (resp, importObject) => { 3 | const source = await (await resp).arrayBuffer(); 4 | return await WebAssembly.instantiate(source, importObject); 5 | }; 6 | } 7 | 8 | const go = new Go(); 9 | WebAssembly.instantiateStreaming(fetch("frontend.wasm"), go.importObject).then( 10 | async result => { 11 | mod = result.module; 12 | inst = result.instance; 13 | await go.run(inst); 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /examples/frontend/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | background-color: #000000; 7 | color: #FFFFFF; 8 | font-family: Arial, Helvetica, sans-serif 9 | } 10 | 11 | #playground { 12 | display: block; 13 | margin-left: auto; 14 | margin-right: auto; 15 | } 16 | -------------------------------------------------------------------------------- /examples/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/web" 4 | 5 | func main() { 6 | window := web.GetWindow() 7 | doc := window.Document() 8 | doc.SetTitle("Welcome page") 9 | 10 | // create

11 | header := doc.CreateElement("h1") 12 | header.SetText("Hello!") 13 | 14 | // add the element into 15 | body := doc.Body() 16 | body.Node().AppendChild(header.Node()) 17 | } 18 | -------------------------------------------------------------------------------- /examples/http_request/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/web" 4 | 5 | func main() { 6 | window := web.GetWindow() 7 | doc := window.Document() 8 | doc.SetTitle("Making HTTP requests") 9 | 10 | // make request 11 | req := window.HTTPRequest("GET", "https://httpbin.org/get") 12 | resp := req.Send(nil) 13 | 14 | header := doc.CreateElement("pre") 15 | header.SetText(string(resp.Body())) 16 | body := doc.Body() 17 | body.Node().AppendChild(header.Node()) 18 | } 19 | -------------------------------------------------------------------------------- /examples/index.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | GWeb Examples 13 | 14 | 15 | 16 | 18 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |

GWeb: golang + js + wasm

28 |

29 | 30 | github.com/life4/gweb 31 | 32 |

33 | {% for category in categories %} 34 |

{{ category['name'] }}

35 | 36 | 37 | {% for item in category['items'] %} 38 | 39 | 42 | 47 | 52 | 57 | 58 | {% endfor %} 59 | 60 |
40 | {{ item['name'] }} 41 | 43 | 44 | source 45 | 46 | 48 | 49 | demo 50 | 51 | 53 | {% markdown %} 54 | {{ item['info'] }} 55 | {% endmarkdown %} 56 |
61 | {% endfor %} 62 |
63 |
64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/index.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - name: DOM 3 | items: 4 | - name: hello 5 | info: 'a small "hello world": set title, create element, add element onto the page.' 6 | - name: styling 7 | info: "how to set CSS attributes for an object." 8 | - name: events 9 | info: how to handle events like mouse movement. 10 | - name: templates 11 | info: > 12 | how to work with `` and 13 | [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). 14 | - name: bootstrap 15 | info: > 16 | how to dynamically load CSS (on example of 17 | [Bootstrap](https://getbootstrap.com/)) 18 | and do something when it is ready. 19 | - name: Canvas 20 | items: 21 | - name: triangle 22 | info: '"hello world" for ``: make black background and draw a red triangle.' 23 | - name: pacman 24 | info: > 25 | ported 26 | [MDN example](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Making_combinations) 27 | of drawing a scene from Pacman. 28 | - name: draw 29 | info: "an example of handling events on canvas and calculating FPS." 30 | - name: ball 31 | info: "a simple game on canvas with moving and bouncing ball and reaction on clicks." 32 | - name: breakout 33 | info: > 34 | port of 35 | [Breakout](http://tiny.cc/5t11jz) 36 | classic video game (famous because of clone 37 | [Arkanoid](https://en.wikipedia.org/wiki/Arkanoid)) 38 | with a bit more natural (hence annoying) physic. 39 | - name: Audio 40 | items: 41 | - name: oscilloscope 42 | info: "a small example of visualization of an audio from the user microphone." 43 | - name: piano 44 | info: > 45 | play MIDI music! A good example of rendering sounds. Based on 46 | [Simple synth keyboard](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth) 47 | MDN example. 48 | - name: Networking 49 | items: 50 | - name: http_request 51 | info: how to make an HTTP request 52 | -------------------------------------------------------------------------------- /examples/oscilloscope/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | 7 | "github.com/life4/gweb/audio" 8 | 9 | "github.com/life4/gweb/canvas" 10 | "github.com/life4/gweb/web" 11 | ) 12 | 13 | const BGColor = "#2c3e50" 14 | const LineColor = "#2ecc71" 15 | 16 | // Scope is an interface that provides information about domain and frequency sequences 17 | // to the Painter. 18 | type Scope interface { 19 | Size() int 20 | Data() []byte 21 | GetY(value byte, height int) int 22 | } 23 | 24 | type Painter struct { 25 | context canvas.Context2D 26 | width int 27 | height int 28 | } 29 | 30 | func (painter *Painter) handle(scope Scope) { 31 | // make background (and remove prev results) 32 | painter.context.SetFillStyle(BGColor) 33 | painter.context.BeginPath() 34 | painter.context.Rectangle(0, 0, painter.width, painter.height).Filled().Draw() 35 | painter.context.ClosePath() 36 | painter.context.MoveTo(0, painter.height/2) 37 | 38 | // don't draw the line if TimeDomain hasn't been initialized yet 39 | if scope.Size() == 0 { 40 | return 41 | } 42 | 43 | // draw the line 44 | chunkWidth := float64(painter.width) / float64(scope.Size()) 45 | painter.context.SetFillStyle(LineColor) 46 | painter.context.Line().SetWidth(2) 47 | x := 0.0 48 | for _, freq := range scope.Data() { 49 | y := scope.GetY(freq, painter.height) 50 | painter.context.LineTo(int(math.Round(x)), y) 51 | x += chunkWidth 52 | } 53 | painter.context.LineTo(painter.width, painter.height/2) 54 | painter.context.Stroke() 55 | } 56 | 57 | // ScopeDomain implements Scope interface for TimeDomainBytes 58 | type ScopeDomain struct { 59 | data *audio.TimeDomainBytes 60 | } 61 | 62 | func (scope *ScopeDomain) Data() []byte { 63 | scope.data.Update() 64 | return scope.data.Data 65 | } 66 | 67 | func (scope *ScopeDomain) Size() int { 68 | return scope.data.Size 69 | } 70 | 71 | func (scope *ScopeDomain) GetY(value byte, height int) int { 72 | return height - int(value)*height/256 73 | } 74 | 75 | // ScopeFreq implements Scope interface for FrequencyDataBytes 76 | type ScopeFreq struct { 77 | data *audio.FrequencyDataBytes 78 | } 79 | 80 | func (scope *ScopeFreq) Data() []byte { 81 | scope.data.Update() 82 | return scope.data.Data 83 | } 84 | 85 | func (scope *ScopeFreq) Size() int { 86 | return scope.data.Size 87 | } 88 | 89 | func (scope *ScopeFreq) GetY(value byte, height int) int { 90 | return height - 10 - int(value)*(height-10)/256 91 | } 92 | 93 | func makeCanvas(w, h int) canvas.Context2D { 94 | window := web.GetWindow() 95 | doc := window.Document() 96 | body := doc.Body() 97 | 98 | // create canvas 99 | canvas := doc.CreateCanvas() 100 | canvas.SetHeight(h) 101 | canvas.SetWidth(w) 102 | body.Node().AppendChild(canvas.Node()) 103 | 104 | context := canvas.Context2D() 105 | 106 | // draw background 107 | context.SetFillStyle(BGColor) 108 | context.Rectangle(0, 0, w, h).Filled().Draw() 109 | 110 | return context 111 | } 112 | 113 | func main() { 114 | window := web.GetWindow() 115 | doc := window.Document() 116 | doc.SetTitle("Audio visualization example") 117 | 118 | // size of canvases, both are a bit smaller than half of the screen (by height) 119 | h := window.InnerHeight()/2 - 40 120 | w := window.InnerWidth() - 40 121 | 122 | var domain audio.TimeDomainBytes 123 | var freq audio.FrequencyDataBytes 124 | 125 | go func() { 126 | // get audio stream from mic 127 | promise := window.Navigator().MediaDevices().Audio() 128 | msg, err := promise.Get() 129 | if err.Truthy() { 130 | window.Console().Error("", err) 131 | } 132 | stream := msg.MediaStream() 133 | 134 | // make analyzer and update time domain and frequency managers 135 | audioContext := window.AudioContext() 136 | analyser := audioContext.Analyser() 137 | analyser.SetMinDecibels(-90) 138 | analyser.SetMaxDecibels(-10) 139 | analyser.SetSmoothingTimeConstant(0.85) 140 | analyser.SetFFTSize(1024) 141 | domain = analyser.TimeDomain() 142 | freq = analyser.FrequencyData() 143 | 144 | // connect audio context to the stream 145 | source := audioContext.MediaStreamSource(stream) 146 | source.Connect(analyser.AudioNode, 0, 0) 147 | }() 148 | 149 | // make domain data painting handler 150 | scopeD := ScopeDomain{ 151 | data: &domain, 152 | } 153 | painterD := Painter{ 154 | context: makeCanvas(w, h), 155 | width: w, 156 | height: h, 157 | } 158 | 159 | // make frequency data painting handler 160 | scopeF := ScopeFreq{ 161 | data: &freq, 162 | } 163 | painterF := Painter{ 164 | context: makeCanvas(w, h), 165 | width: w, 166 | height: h, 167 | } 168 | 169 | // register handlers 170 | handle := func() { 171 | wg := sync.WaitGroup{} 172 | wg.Add(2) 173 | go func() { 174 | painterD.handle(&scopeD) 175 | wg.Done() 176 | }() 177 | go func() { 178 | painterF.handle(&scopeF) 179 | wg.Done() 180 | }() 181 | wg.Wait() 182 | } 183 | window.RequestAnimationFrame(handle, true) 184 | // prevent ending of the program 185 | select {} 186 | } 187 | -------------------------------------------------------------------------------- /examples/pacman/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/life4/gweb/web" 7 | ) 8 | 9 | // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Making_combinations 10 | func main() { 11 | window := web.GetWindow() 12 | doc := window.Document() 13 | doc.SetTitle("Canvas pacman example") 14 | body := doc.Body() 15 | 16 | // create canvas 17 | canvas := doc.CreateCanvas() 18 | canvas.SetHeight(150) 19 | canvas.SetWidth(150) 20 | body.Node().AppendChild(canvas.Node()) 21 | 22 | ctx := canvas.Context2D() 23 | 24 | // make background 25 | ctx.SetFillStyle("#ecf0f1") 26 | ctx.Rectangle(0, 0, 150, 150).Filled().Draw() 27 | 28 | // draw walls 29 | ctx.SetFillStyle("#2c3e50") 30 | ctx.Rectangle(12, 12, 150, 150).Rounded(15).Draw() 31 | ctx.Rectangle(19, 19, 150, 150).Rounded(9).Draw() 32 | ctx.Rectangle(53, 53, 49, 33).Rounded(10).Draw() 33 | ctx.Rectangle(53, 119, 49, 16).Rounded(6).Draw() 34 | ctx.Rectangle(135, 53, 49, 33).Rounded(10).Draw() 35 | ctx.Rectangle(135, 119, 25, 49).Rounded(10).Draw() 36 | 37 | // draw pacman body 38 | ctx.SetFillStyle("#f39c12") 39 | ctx.BeginPath() 40 | ctx.Arc(37, 37, 13, math.Pi/7, -math.Pi/7) 41 | ctx.LineTo(31, 37) 42 | ctx.Fill() 43 | 44 | // draw bread crumbs 45 | ctx.SetFillStyle("#2c3e50") 46 | for i := 0; i < 8; i++ { 47 | ctx.Rectangle(51+i*16, 35, 4, 4).Filled().Draw() 48 | } 49 | for i := 0; i < 6; i++ { 50 | ctx.Rectangle(115, 51+i*16, 4, 4).Filled().Draw() 51 | } 52 | 53 | for i := 0; i < 8; i++ { 54 | ctx.Rectangle(51+i*16, 99, 4, 4).Filled().Draw() 55 | } 56 | 57 | // draw ghost's body 58 | ctx.BeginPath() 59 | ctx.MoveTo(83, 116) 60 | ctx.LineTo(83, 102) 61 | ctx.BezierCurveTo(83, 94, 89, 88, 97, 88) 62 | ctx.BezierCurveTo(105, 88, 111, 94, 111, 102) 63 | ctx.LineTo(111, 116) 64 | ctx.LineTo(106, 111) 65 | ctx.LineTo(101, 116) 66 | ctx.LineTo(97, 111) 67 | ctx.LineTo(92, 116) 68 | ctx.LineTo(87, 111) 69 | ctx.LineTo(83, 116) 70 | ctx.Fill() 71 | 72 | // draw ghost's eyes 73 | ctx.SetFillStyle("white") 74 | ctx.BeginPath() 75 | ctx.MoveTo(91, 96) 76 | ctx.BezierCurveTo(88, 96, 87, 99, 87, 101) 77 | ctx.BezierCurveTo(87, 103, 88, 106, 91, 106) 78 | ctx.BezierCurveTo(94, 106, 95, 103, 95, 101) 79 | ctx.BezierCurveTo(95, 99, 94, 96, 91, 96) 80 | ctx.MoveTo(103, 96) 81 | ctx.BezierCurveTo(100, 96, 99, 99, 99, 101) 82 | ctx.BezierCurveTo(99, 103, 100, 106, 103, 106) 83 | ctx.BezierCurveTo(106, 106, 107, 103, 107, 101) 84 | ctx.BezierCurveTo(107, 99, 106, 96, 103, 96) 85 | ctx.Fill() 86 | 87 | // draw ghost's pupils 88 | ctx.SetFillStyle("black") 89 | ctx.BeginPath() 90 | ctx.Arc(101, 102, 2, 0, math.Pi*2) 91 | ctx.Fill() 92 | ctx.BeginPath() 93 | ctx.Arc(89, 102, 2, 0, math.Pi*2) 94 | ctx.Fill() 95 | } 96 | -------------------------------------------------------------------------------- /examples/piano/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/life4/gweb/web" 9 | ) 10 | 11 | type Key struct { 12 | Octave int 13 | Note string 14 | element web.HTMLElement 15 | } 16 | 17 | func (key Key) Press() { 18 | key.element.Style().SetBackgroundColor("#2980b9", false) 19 | key.element.Style().SetColor("#ecf0f1", false) 20 | } 21 | 22 | func (key Key) Release() { 23 | if strings.Contains(key.Note, "#") { 24 | key.element.Style().SetBackgroundColor("#7f8c8d", false) 25 | } else { 26 | key.element.Style().SetBackgroundColor("#2c3e50", false) 27 | } 28 | key.element.Style().SetColor("#bdc3c7", false) 29 | } 30 | 31 | func (key *Key) Render(doc web.Document) web.HTMLElement { 32 | element := doc.CreateElement("span") 33 | element.SetText(key.Note) 34 | element.SetID(fmt.Sprintf("key-%d-%s", key.Octave, strings.ReplaceAll(key.Note, "#", "s"))) 35 | element = StyleBlock(element) 36 | 37 | key.element = element 38 | key.Release() 39 | return element 40 | } 41 | 42 | func KeyFromElement(element web.HTMLElement) Key { 43 | parts := strings.Split(element.ID(), "-") 44 | octave, _ := strconv.Atoi(parts[1]) 45 | note := strings.ReplaceAll(parts[2], "s", "#") 46 | return Key{ 47 | element: element, 48 | Octave: octave, 49 | Note: note, 50 | } 51 | } 52 | 53 | func KeyFromNote(doc web.Document, octave int, note string) Key { 54 | id := fmt.Sprintf("key-%d-%s", octave, strings.ReplaceAll(note, "#", "s")) 55 | element := doc.Element(id) 56 | if !element.Truthy() { 57 | return Key{} 58 | } 59 | return Key{ 60 | element: element, 61 | Octave: octave, 62 | Note: note, 63 | } 64 | } 65 | 66 | func StyleBlock(element web.HTMLElement) web.HTMLElement { 67 | element.Style().SetDisplay("inline-block", false) 68 | element.Style().SetWidth("40px", false) 69 | element.Style().SetTextAlign("center", false) 70 | element.Style().SetMargin("2px", false) 71 | return element 72 | } 73 | -------------------------------------------------------------------------------- /examples/piano/keyboard.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/life4/gweb/audio" 7 | "github.com/life4/gweb/web" 8 | ) 9 | 10 | type KeyBoard struct { 11 | notes map[int]map[string]float64 12 | context audio.AudioContext 13 | doc web.Document 14 | sounds map[int]map[string]*Sound 15 | octave int 16 | } 17 | 18 | func (kbd KeyBoard) Octaves() []int { 19 | max := 0 20 | for octave := range kbd.notes { 21 | if octave > max { 22 | max = octave 23 | } 24 | } 25 | result := make([]int, max+1) 26 | for n := 0; n <= max; n++ { 27 | result[n] = n 28 | } 29 | return result 30 | } 31 | 32 | func (kbd KeyBoard) Notes() []string { 33 | return []string{"A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"} 34 | } 35 | 36 | func (kbd KeyBoard) Render(doc web.Document) web.HTMLElement { 37 | root := doc.CreateElement("div") 38 | for _, octave := range kbd.Octaves() { 39 | row := doc.CreateElement("div") 40 | row.SetID(fmt.Sprintf("octave-%d", octave)) 41 | 42 | number := doc.CreateElement("span") 43 | number.SetText(fmt.Sprintf("%d", octave)) 44 | number = StyleBlock(number) 45 | row.Node().AppendChild(number.Node()) 46 | 47 | for _, note := range kbd.Notes() { 48 | _, ok := kbd.notes[octave][note] 49 | if !ok { 50 | holder := doc.CreateElement("span") 51 | holder = StyleBlock(holder) 52 | row.Node().AppendChild(holder.Node()) 53 | continue 54 | } 55 | 56 | key := Key{Octave: octave, Note: note} 57 | element := key.Render(doc) 58 | element.EventTarget().Listen(web.EventTypeMouseDown, kbd.handlePress) 59 | // element.EventTarget().Listen(web.EventTypeMouseOver, kbd.handlePress) 60 | element.EventTarget().Listen(web.EventTypeMouseUp, kbd.handleRelease) 61 | element.EventTarget().Listen(web.EventTypeMouseLeave, kbd.handleRelease) 62 | row.Node().AppendChild(element.Node()) 63 | } 64 | 65 | root.Node().AppendChild(row.Node()) 66 | } 67 | 68 | doc.EventTarget().Listen(web.EventTypeKeyDown, kbd.handleKeyDown) 69 | doc.EventTarget().Listen(web.EventTypeKeyUp, kbd.handleKeyUp) 70 | kbd.doc = doc 71 | return root 72 | } 73 | 74 | func (kbd KeyBoard) play(octave int, note string) Sound { 75 | freq := kbd.notes[octave][note] 76 | return Play(kbd.context, freq) 77 | } 78 | 79 | func (kbd *KeyBoard) Press(octave int, note string) { 80 | old, ok := kbd.sounds[octave][note] 81 | if ok && old != nil { 82 | return 83 | } 84 | 85 | sound := kbd.play(octave, note) 86 | sounds := kbd.sounds[octave] 87 | if sounds == nil { 88 | kbd.sounds[octave] = make(map[string]*Sound) 89 | } 90 | kbd.sounds[octave][note] = &sound 91 | } 92 | 93 | func (kbd *KeyBoard) Release(octave int, note string) { 94 | sound, ok := kbd.sounds[octave][note] 95 | if !ok || sound == nil { 96 | return 97 | } 98 | sound.Stop() 99 | kbd.sounds[octave][note] = nil 100 | } 101 | 102 | func (kbd *KeyBoard) SetOctave(octave int) { 103 | if octave == kbd.octave { 104 | return 105 | } 106 | mod := len(kbd.Octaves()) 107 | kbd.octave = (mod + octave) % mod 108 | 109 | // if octave has been changed, release all pressed keys 110 | for octave, sounds := range kbd.sounds { 111 | for note := range sounds { 112 | key := KeyFromNote(kbd.doc, octave, note) 113 | key.Release() 114 | kbd.Release(octave, note) 115 | } 116 | } 117 | } 118 | 119 | // handlers 120 | 121 | func (kbd *KeyBoard) handlePress(event web.Event) { 122 | element := event.CurrentTarget().HTMLElement() 123 | key := KeyFromElement(element) 124 | key.Press() 125 | kbd.Press(key.Octave, key.Note) 126 | } 127 | 128 | func (kbd *KeyBoard) handleRelease(event web.Event) { 129 | element := event.CurrentTarget().HTMLElement() 130 | key := KeyFromElement(element) 131 | key.Release() 132 | kbd.Release(key.Octave, key.Note) 133 | } 134 | 135 | func (kbd *KeyBoard) handleKeyDown(event web.Event) { 136 | keyCode := event.Get("keyCode").Int() 137 | 138 | // change octave if arrow up or down is pressed 139 | if keyCode == 38 { 140 | kbd.SetOctave(kbd.octave - 1) 141 | return 142 | } 143 | if keyCode == 40 { 144 | kbd.SetOctave(kbd.octave + 1) 145 | return 146 | } 147 | // change octave on numbers pressed 148 | mod := len(kbd.Octaves()) 149 | if keyCode >= 48 && keyCode <= 48+mod { 150 | kbd.SetOctave(keyCode - 48) 151 | } 152 | 153 | note, offset := keyToNote(keyCode) 154 | if note == "" { 155 | return 156 | } 157 | octave := (mod + kbd.octave + offset) % mod 158 | key := KeyFromNote(kbd.doc, octave, note) 159 | 160 | // if no key for the given note 161 | if key.Note == "" { 162 | return 163 | } 164 | 165 | key.Press() 166 | kbd.Press(octave, note) 167 | } 168 | 169 | func (kbd *KeyBoard) handleKeyUp(event web.Event) { 170 | keyCode := event.Get("keyCode").Int() 171 | mod := len(kbd.Octaves()) 172 | 173 | note, offset := keyToNote(keyCode) 174 | if note == "" { 175 | return 176 | } 177 | octave := (mod + kbd.octave + offset) % mod 178 | key := KeyFromNote(kbd.doc, octave, note) 179 | 180 | // if no key for the given note 181 | if key.Note == "" { 182 | return 183 | } 184 | key.Release() 185 | kbd.Release(octave, note) 186 | } 187 | 188 | // funcs 189 | 190 | func getNotes() map[int]map[string]float64 { 191 | notes := make(map[int]map[string]float64) 192 | notes[0] = map[string]float64{ 193 | "A": 27.500000000000000, 194 | "A#": 29.135235094880619, 195 | "B": 30.867706328507756, 196 | } 197 | notes[1] = map[string]float64{ 198 | "C": 32.703195662574829, 199 | "C#": 34.647828872109012, 200 | "D": 36.708095989675945, 201 | "D#": 38.890872965260113, 202 | "E": 41.203444614108741, 203 | "F": 43.653528929125485, 204 | "F#": 46.249302838954299, 205 | "G": 48.999429497718661, 206 | "G#": 51.913087197493142, 207 | "A": 55.000000000000000, 208 | "A#": 58.270470189761239, 209 | "B": 61.735412657015513, 210 | } 211 | notes[2] = map[string]float64{ 212 | "C": 65.406391325149658, 213 | "C#": 69.295657744218024, 214 | "D": 73.416191979351890, 215 | "D#": 77.781745930520227, 216 | "E": 82.406889228217482, 217 | "F": 87.307057858250971, 218 | "F#": 92.498605677908599, 219 | "G": 97.998858995437323, 220 | "G#": 103.826174394986284, 221 | "A": 110.000000000000000, 222 | "A#": 116.540940379522479, 223 | "B": 123.470825314031027, 224 | } 225 | 226 | notes[3] = map[string]float64{ 227 | "C": 130.812782650299317, 228 | "C#": 138.591315488436048, 229 | "D": 146.832383958703780, 230 | "D#": 155.563491861040455, 231 | "E": 164.813778456434964, 232 | "F": 174.614115716501942, 233 | "F#": 184.997211355817199, 234 | "G": 195.997717990874647, 235 | "G#": 207.652348789972569, 236 | "A": 220.000000000000000, 237 | "A#": 233.081880759044958, 238 | "B": 246.941650628062055, 239 | } 240 | 241 | notes[4] = map[string]float64{ 242 | "C": 261.625565300598634, 243 | "C#": 277.182630976872096, 244 | "D": 293.664767917407560, 245 | "D#": 311.126983722080910, 246 | "E": 329.627556912869929, 247 | "F": 349.228231433003884, 248 | "F#": 369.994422711634398, 249 | "G": 391.995435981749294, 250 | "G#": 415.304697579945138, 251 | "A": 440.000000000000000, 252 | "A#": 466.163761518089916, 253 | "B": 493.883301256124111, 254 | } 255 | 256 | notes[5] = map[string]float64{ 257 | "C": 523.251130601197269, 258 | "C#": 554.365261953744192, 259 | "D": 587.329535834815120, 260 | "D#": 622.253967444161821, 261 | "E": 659.255113825739859, 262 | "F": 698.456462866007768, 263 | "F#": 739.988845423268797, 264 | "G": 783.990871963498588, 265 | "G#": 830.609395159890277, 266 | "A": 880.000000000000000, 267 | "A#": 932.327523036179832, 268 | "B": 987.766602512248223, 269 | } 270 | 271 | notes[6] = map[string]float64{ 272 | "C": 1046.502261202394538, 273 | "C#": 1108.730523907488384, 274 | "D": 1174.659071669630241, 275 | "D#": 1244.507934888323642, 276 | "E": 1318.510227651479718, 277 | "F": 1396.912925732015537, 278 | "F#": 1479.977690846537595, 279 | "G": 1567.981743926997176, 280 | "G#": 1661.218790319780554, 281 | "A": 1760.000000000000000, 282 | "A#": 1864.655046072359665, 283 | "B": 1975.533205024496447, 284 | } 285 | 286 | notes[7] = map[string]float64{ 287 | "C": 2093.004522404789077, 288 | "C#": 2217.461047814976769, 289 | "D": 2349.318143339260482, 290 | "D#": 2489.015869776647285, 291 | "E": 2637.020455302959437, 292 | "F": 2793.825851464031075, 293 | "F#": 2959.955381693075191, 294 | "G": 3135.963487853994352, 295 | "G#": 3322.437580639561108, 296 | "A": 3520.000000000000000, 297 | "A#": 3729.310092144719331, 298 | "B": 3951.066410048992894, 299 | } 300 | 301 | notes[8] = map[string]float64{ 302 | "C": 4186.009044809578154, 303 | } 304 | return notes 305 | } 306 | 307 | func keyToNote(key int) (string, int) { 308 | switch key + 32 { 309 | case int('z'): 310 | return "A", 1 311 | case int('x'): 312 | return "B", 1 313 | case int('c'): 314 | return "C", 1 315 | case int('v'): 316 | return "D", 1 317 | case int('b'): 318 | return "E", 1 319 | case int('n'): 320 | return "F", 1 321 | case int('m'): 322 | return "G", 1 323 | } 324 | 325 | switch key + 32 { 326 | case int('a'): 327 | return "A", 0 328 | case int('s'): 329 | return "B", 0 330 | case int('d'): 331 | return "C", 0 332 | case int('f'): 333 | return "D", 0 334 | case int('g'): 335 | return "E", 0 336 | case int('h'): 337 | return "F", 0 338 | case int('j'): 339 | return "G", 0 340 | } 341 | 342 | switch key + 32 { 343 | case int('q'): 344 | return "A", -1 345 | case int('w'): 346 | return "B", -1 347 | case int('e'): 348 | return "C", -1 349 | case int('r'): 350 | return "D", -1 351 | case int('t'): 352 | return "E", -1 353 | case int('y'): 354 | return "F", -1 355 | case int('u'): 356 | return "G", -1 357 | } 358 | 359 | return "", 0 360 | } 361 | -------------------------------------------------------------------------------- /examples/piano/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/life4/gweb/audio" 5 | "github.com/life4/gweb/web" 6 | ) 7 | 8 | func main() { 9 | window := web.GetWindow() 10 | doc := window.Document() 11 | body := doc.Body() 12 | 13 | audioContext := window.AudioContext() 14 | if audioContext.State() != audio.AudioContextStateRunning { 15 | audioContext.Resume() 16 | } 17 | dest := audioContext.Destination() 18 | gain := audioContext.Gain() 19 | gain.Connect(dest.AudioNode, 0, 0) 20 | gain.Gain().Set(1.0) 21 | 22 | keyboard := KeyBoard{ 23 | notes: getNotes(), 24 | context: audioContext, 25 | sounds: make(map[int]map[string]*Sound), 26 | octave: 3, 27 | } 28 | element := keyboard.Render(doc) 29 | body.Node().AppendChild(element.Node()) 30 | 31 | select {} 32 | } 33 | -------------------------------------------------------------------------------- /examples/piano/sound.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/life4/gweb/audio" 5 | ) 6 | 7 | type Sound struct { 8 | gain audio.GainNode 9 | osc audio.OscillatorNode 10 | context audio.AudioContext 11 | } 12 | 13 | func Play(context audio.AudioContext, freq float64) Sound { 14 | dest := context.Destination() 15 | gain := context.Gain() 16 | gain.Connect(dest.AudioNode, 0, 0) 17 | gain.Gain().Set(1.0) 18 | 19 | osc := context.Oscillator() 20 | osc.Connect(gain.AudioNode, 0, 0) 21 | osc.SetShape(audio.ShapeTriangle) 22 | osc.Frequency().Set(freq) 23 | osc.Start(0) 24 | 25 | return Sound{ 26 | gain: gain, 27 | osc: osc, 28 | context: context, 29 | } 30 | } 31 | 32 | func (sound *Sound) Stop() { 33 | // fade out 34 | time := sound.context.CurrentTime() + 2 35 | sound.gain.Gain().AtTime(time).ExponentialRampTo(0.01) 36 | sound.osc.Stop(time) 37 | } 38 | -------------------------------------------------------------------------------- /examples/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | ./build.sh $1 4 | go build -o server.bin ./server/ 5 | ./server.bin 6 | -------------------------------------------------------------------------------- /examples/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | listen = flag.String("listen", "127.0.0.1:1337", "listen address") 11 | dir = flag.String("dir", "build", "directory to serve") 12 | ) 13 | 14 | func main() { 15 | flag.Parse() 16 | log.Printf("listening on %q...", *listen) 17 | err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))) 18 | log.Fatalln(err) 19 | } 20 | -------------------------------------------------------------------------------- /examples/styling/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/web" 4 | 5 | func main() { 6 | window := web.GetWindow() 7 | doc := window.Document() 8 | doc.SetTitle("Styling example") 9 | 10 | // create

11 | paragraph := doc.CreateElement("p") 12 | paragraph.SetText("Styled!") 13 | 14 | // make it cool 15 | style := paragraph.Style() 16 | style.SetColor("purple", false) 17 | style.SetFontFamily("Comic Sans MS", false) 18 | style.SetFontSize("2em", false) 19 | 20 | // add the element into 21 | body := doc.Body() 22 | body.Node().AppendChild(paragraph.Node()) 23 | } 24 | -------------------------------------------------------------------------------- /examples/templates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/web" 4 | 5 | func main() { 6 | window := web.GetWindow() 7 | doc := window.Document() 8 | doc.SetTitle("Templates example") 9 | body := doc.Body() 10 | 11 | // create template 12 | template := doc.CreateElement("div") 13 | template.Style().SetBorder("solid 4px red", false) 14 | 15 | // add into template 16 | slot := doc.CreateElement("slot") 17 | slot.Set("name", "example") // here we call syscall/js-like method 18 | slot.SetInnerHTML("default text") 19 | template.Node().AppendChild(slot.Node()) 20 | 21 | // Add template into a shadow DOM. 22 | // This is the most important thing to make the template renderable 23 | shadow := body.Shadow().Attach() 24 | // since we clone the template, we should add all 's before it. 25 | shadow.Node().AppendChild(template.Node().Clone(true)) 26 | 27 | // make element that will replace the 28 | span := doc.CreateElement("span") 29 | span.SetText("The template is rendered!") 30 | span.SetSlot("example") 31 | 32 | // add into , and it will automatically fill 33 | body.Node().AppendChild(span.Node()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/triangle/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/life4/gweb/web" 4 | 5 | func main() { 6 | window := web.GetWindow() 7 | doc := window.Document() 8 | doc.SetTitle("Canvas triangle example") 9 | body := doc.Body() 10 | 11 | // create canvas 12 | h := window.InnerHeight() - 40 13 | w := window.InnerWidth() - 40 14 | canvas := doc.CreateCanvas() 15 | canvas.SetHeight(h) 16 | canvas.SetWidth(w) 17 | body.Node().AppendChild(canvas.Node()) 18 | 19 | context := canvas.Context2D() 20 | 21 | // draw black background 22 | context.SetFillStyle("black") 23 | context.Rectangle(0, 0, w, h).Filled().Draw() 24 | 25 | // draw red triangle 26 | centerX := w / 2 27 | centerY := h / 2 28 | context.SetFillStyle("red") 29 | context.BeginPath() 30 | context.MoveTo(centerX-40, centerY+40) 31 | context.LineTo(centerX+40, centerY+40) 32 | context.LineTo(centerX, centerY-40) 33 | context.Fill() 34 | context.ClosePath() 35 | } 36 | -------------------------------------------------------------------------------- /generate_refs.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | from pathlib import Path 4 | 5 | base_url = 'https://developer.mozilla.org/en-US/docs/Web/API/' 6 | doc_base_url = 'https://pkg.go.dev/github.com/life4/gweb/{package}#{obj}' 7 | link = re.escape(f'// {base_url}') 8 | rex = re.compile(rf'(?:{link}([a-zA-Z/-]+))+\nfunc \([a-z]+ \*?([a-zA-Z]+)\) ([a-zA-Z]+)') 9 | 10 | refs: dict = defaultdict(list) 11 | for path in Path().glob('*/*.go'): 12 | content = path.read_text() 13 | for match in rex.findall(content): 14 | *links, struct, func = match 15 | for link in links: 16 | refs[link].append((path.parent.name, f'{struct}.{func}')) 17 | 18 | print(""" 19 | # Reference 20 | 21 | Below is the mapping of web API to gweb functions. 22 | This file is autogenerated, so some references may be missed. 23 | 24 | | Web API | gweb | 25 | | ------- | ---- | 26 | """.strip()) 27 | for ref, objects in sorted(refs.items()): 28 | url = base_url + ref 29 | ref = ref.replace('/', '.') 30 | for package, obj in objects: 31 | doc_url = doc_base_url.format(package=package, obj=obj) 32 | print(f'| [{ref}]({url}) | [{obj}]({doc_url}) |') 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/life4/gweb 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/agnivade/wasmbrowsertest v0.7.0 7 | github.com/stretchr/testify v1.7.0 8 | ) 9 | 10 | require ( 11 | github.com/chromedp/cdproto v0.0.0-20230828023241-f357fd93b5d6 // indirect 12 | github.com/chromedp/chromedp v0.9.2 // indirect 13 | github.com/chromedp/sysutil v1.0.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-interpreter/wagon v0.6.0 // indirect 16 | github.com/gobwas/httphead v0.1.0 // indirect 17 | github.com/gobwas/pool v0.2.1 // indirect 18 | github.com/gobwas/ws v1.3.0 // indirect 19 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect 20 | github.com/josharian/intern v1.0.0 // indirect 21 | github.com/kr/text v0.2.0 // indirect 22 | github.com/mailru/easyjson v0.7.7 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | golang.org/x/sys v0.11.0 // indirect 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agnivade/wasmbrowsertest v0.7.0 h1:vgi7PKcYHBI13PBpefjtObJl3xQKYmcu3JbAQd4z79s= 2 | github.com/agnivade/wasmbrowsertest v0.7.0/go.mod h1:kvkdPoZxkikAxwXLc0uW2c5iKhjP8//lmC9LA5hl8rU= 3 | github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0= 4 | github.com/chromedp/cdproto v0.0.0-20221108233440-fad8339618ab/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= 5 | github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= 6 | github.com/chromedp/cdproto v0.0.0-20230828023241-f357fd93b5d6 h1:lyUj4I0kT1UjLOHtAY1Pbx5rH9LfGFXhvY8oWmuIZ1w= 7 | github.com/chromedp/cdproto v0.0.0-20230828023241-f357fd93b5d6/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= 8 | github.com/chromedp/chromedp v0.8.6/go.mod h1:nBYHoD6YSNzrr82cIeuOzhw1Jo/s2o0QQ+ifTeoCZ+c= 9 | github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= 10 | github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= 11 | github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= 12 | github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= 13 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 14 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 15 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= 21 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 22 | github.com/go-interpreter/wagon v0.6.0 h1:BBxDxjiJiHgw9EdkYXAWs8NHhwnazZ5P2EWBW5hFNWw= 23 | github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= 24 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 25 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 26 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 27 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 28 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 29 | github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 30 | github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= 31 | github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 32 | github.com/google/pprof v0.0.0-20221103000818-d260c55eee4c/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 33 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= 34 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 35 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 36 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 37 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 38 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 39 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 42 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= 45 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 46 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 47 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 48 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= 49 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 53 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc h1:RTUQlKzoZZVG3umWNzOYeFecQLIh+dbxXvJp1zPQJTI= 56 | github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= 57 | golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 58 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 63 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 66 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public/" 3 | command = "./examples/build_all.py" 4 | environment = {GO_VERSION = "1.18", PYTHON_VERSION = "3.8"} 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements for netlify to run examples/build_all.py 2 | jinja2 3 | jinja2_markdown 4 | pyyaml 5 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package main 5 | 6 | import ( 7 | _ "github.com/agnivade/wasmbrowsertest" 8 | ) 9 | -------------------------------------------------------------------------------- /web/canvas.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import "github.com/life4/gweb/canvas" 4 | 5 | // Canvas provides properties and methods for manipulating the layout and presentation of elements. 6 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement 7 | type Canvas struct { 8 | HTMLElement 9 | } 10 | 11 | // getters 12 | 13 | // Context returns a drawing context on the canvas, or null if the context ID is not supported. 14 | // A drawing context lets you draw on the canvas. 15 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext 16 | func (element Canvas) Context(name string) canvas.Context { 17 | value := element.Call("getContext", name) 18 | return canvas.Context{Value: value.JSValue()} 19 | } 20 | 21 | // Context2D returns 2D context to draw on canvas. 22 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext 23 | func (element Canvas) Context2D() canvas.Context2D { 24 | context := element.Context("2d") 25 | return context.Context2D() 26 | } 27 | 28 | // Width is the width of the element interpreted in CSS pixels 29 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width 30 | func (element Canvas) Width() int { 31 | return element.Get("width").Int() 32 | } 33 | 34 | // Height is the height of the element interpreted in CSS pixels 35 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/height 36 | func (element Canvas) Height() int { 37 | return element.Get("height").Int() 38 | } 39 | 40 | // setters 41 | 42 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width 43 | func (element Canvas) SetWidth(value int) { 44 | element.Set("width", value) 45 | } 46 | 47 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/height 48 | func (element Canvas) SetHeight(value int) { 49 | element.Set("height", value) 50 | } 51 | -------------------------------------------------------------------------------- /web/console.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/API/Console 4 | type Console struct { 5 | Value 6 | } 7 | 8 | // LOGGING 9 | 10 | func (console Console) log(fname, format string, args []any) { 11 | if format == "" { 12 | console.Call(fname, args...) 13 | } else { 14 | console.Call(fname, append([]any{format}, args...)...) 15 | } 16 | } 17 | 18 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/log 19 | func (console Console) Log(format string, args ...any) { 20 | console.log("log", format, args) 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/debug 24 | func (console Console) Debug(format string, args ...any) { 25 | console.log("debug", format, args) 26 | } 27 | 28 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/info 29 | func (console Console) Info(format string, args ...any) { 30 | console.log("info", format, args) 31 | } 32 | 33 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/warn 34 | func (console Console) Warning(format string, args ...any) { 35 | console.log("warn", format, args) 36 | } 37 | 38 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/error 39 | func (console Console) Error(format string, args ...any) { 40 | console.log("error", format, args) 41 | } 42 | 43 | // OTHER METHODS 44 | 45 | func (console Console) callWithLabel(fname, label string) { 46 | if label == "" { 47 | console.Call(fname) 48 | } else { 49 | console.Call(fname, label) 50 | } 51 | } 52 | 53 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/clear 54 | func (console Console) Clear() { 55 | console.Call("clear") 56 | } 57 | 58 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/count 59 | func (console Console) Count(label string) { 60 | console.callWithLabel("count", label) 61 | } 62 | 63 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/countReset 64 | func (console Console) CountReset(label string) { 65 | console.callWithLabel("countReset", label) 66 | } 67 | 68 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/group 69 | func (console Console) Group(label string) { 70 | console.callWithLabel("group", label) 71 | } 72 | 73 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/groupCollapsed 74 | func (console Console) GroupCollapsed(label string) { 75 | console.callWithLabel("groupCollapsed", label) 76 | } 77 | 78 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/groupEnd 79 | func (console Console) GroupEnd() { 80 | console.Call("groupEnd") 81 | } 82 | 83 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/profile 84 | func (console Console) Profile(label string) { 85 | console.callWithLabel("profile", label) 86 | } 87 | 88 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/profileEnd 89 | func (console Console) ProfileEnd(label string) { 90 | console.callWithLabel("profileEnd", label) 91 | } 92 | 93 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/time 94 | func (console Console) Time(label string) { 95 | console.callWithLabel("time", label) 96 | } 97 | 98 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/timeEnd 99 | func (console Console) TimeEnd(label string) { 100 | console.callWithLabel("timeEnd", label) 101 | } 102 | 103 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/timeLog 104 | func (console Console) TimeLog(label string) { 105 | console.callWithLabel("timeLog", label) 106 | } 107 | 108 | // https://developer.mozilla.org/en-US/docs/Web/API/Console/trace 109 | func (console Console) Trace(args ...any) { 110 | console.Call("trace", args...) 111 | } 112 | -------------------------------------------------------------------------------- /web/console_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | -------------------------------------------------------------------------------- /web/document.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "syscall/js" 5 | "time" 6 | ) 7 | 8 | type Document struct { 9 | Value 10 | } 11 | 12 | // SUBTYPE GETTERS 13 | 14 | func (doc *Document) Fullscreen() Fullscreen { 15 | return Fullscreen{value: doc.Value} 16 | } 17 | 18 | func (doc *Document) Node() Node { 19 | return Node{value: doc.Value} 20 | } 21 | 22 | // DOCUMENT STRING PROPERTIES 23 | 24 | // URL returns the URL for the current document. 25 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/URL 26 | func (doc *Document) URL() string { 27 | return doc.Get("URL").String() 28 | } 29 | 30 | // Cookie returns the HTTP cookies that apply to the Document. 31 | // If there are no cookies or cookies can't be applied to this resource, the empty string will be returned. 32 | func (doc *Document) Cookie() string { 33 | return doc.Get("cookie").String() 34 | } 35 | 36 | // CharacterSet returns document's encoding. 37 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/characterSet 38 | func (doc *Document) CharacterSet() string { 39 | return doc.Get("characterSet").String() 40 | } 41 | 42 | // ContentType returns document's content type. 43 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/contentType 44 | func (doc *Document) ContentType() string { 45 | return doc.Get("contentType").String() 46 | } 47 | 48 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/doctype 49 | func (doc *Document) DocType() string { 50 | return doc.Get("doctype").Get("name").String() 51 | } 52 | 53 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/domain 54 | func (doc *Document) Domain() string { 55 | v := doc.Get("domain") 56 | return v.OptionalString() 57 | } 58 | 59 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/referrer 60 | func (doc *Document) Referrer() string { 61 | return doc.Get("referrer").String() 62 | } 63 | 64 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState 65 | func (doc *Document) ReadyState() string { 66 | return doc.Get("readyState").String() 67 | } 68 | 69 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/title 70 | func (doc *Document) Title() string { 71 | return doc.Get("title").String() 72 | } 73 | 74 | // GETTING CONCRETE SUBELEMENTS 75 | 76 | // Body returns the or node of the current document. 77 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/body 78 | func (doc Document) Body() HTMLElement { 79 | return doc.Get("body").HTMLElement() 80 | } 81 | 82 | // Head returns the element of the current document. 83 | func (doc Document) Head() HTMLElement { 84 | return doc.Get("head").HTMLElement() 85 | } 86 | 87 | // HTML returns the Element that is a direct child of the document. 88 | // For HTML documents, this is normally the element. 89 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/head 90 | func (doc Document) HTML() HTMLElement { 91 | return doc.Get("documentElement").HTMLElement() 92 | } 93 | 94 | // Embeds returns and elements in the document. 95 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/embeds 96 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/plugins 97 | func (doc *Document) Embeds() []Embed { 98 | collection := doc.Get("plugins") 99 | values := collection.Values() 100 | 101 | collection = doc.Get("embeds") 102 | values = append(values, collection.Values()...) 103 | 104 | elements := make([]Embed, len(values)) 105 | for i, value := range values { 106 | elements[i] = value.Embed() 107 | } 108 | return elements 109 | } 110 | 111 | // NON-STRING PROPERTIES 112 | 113 | // DesignMode indicates whether the document can be edited. 114 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/designMode 115 | func (doc *Document) DesignMode() bool { 116 | return doc.Get("designMode").String() == "on" 117 | } 118 | 119 | // Hidden is true when the webpage is in the background and not visible to the user 120 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/hidden 121 | func (doc *Document) Hidden() bool { 122 | return doc.Get("hidden").Bool() 123 | } 124 | 125 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/lastModified 126 | func (doc *Document) LastModified() time.Time { 127 | date := doc.Get("lastModified").String() 128 | timestamp := js.Global().Get("Date").Call("parse", date).Float() 129 | return time.Unix(int64(timestamp/1000), 0) 130 | } 131 | 132 | // SETTERS 133 | 134 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/title 135 | func (doc Document) SetTitle(title string) { 136 | doc.Set("title", title) 137 | } 138 | 139 | // METHODS 140 | 141 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement 142 | func (doc Document) CreateElement(name string) HTMLElement { 143 | return doc.Call("createElement", name).HTMLElement() 144 | } 145 | 146 | func (doc Document) CreateCanvas() Canvas { 147 | return doc.CreateElement("canvas").Canvas() 148 | } 149 | 150 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById 151 | func (doc Document) Element(id string) HTMLElement { 152 | return doc.Call("getElementById", id).HTMLElement() 153 | } 154 | 155 | // SUBTYPES 156 | 157 | type Fullscreen struct { 158 | value Value 159 | } 160 | 161 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/fullscreenEnabled 162 | func (scroll *Scroll) Available() bool { 163 | return scroll.value.Get("fullscreenEnabled").Bool() 164 | } 165 | -------------------------------------------------------------------------------- /web/document_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "strings" 5 | "syscall/js" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDocumentURL(t *testing.T) { 13 | d := GetWindow().Document() 14 | assert.True(t, strings.HasPrefix(d.URL(), "http://127.0.0.1:"), "bad URL") 15 | } 16 | 17 | func TestDocumentCookie(t *testing.T) { 18 | d := GetWindow().Document() 19 | assert.Equal(t, d.Cookie(), "", "bad cookie string") 20 | } 21 | 22 | func TestDocumentCharacterSet(t *testing.T) { 23 | d := GetWindow().Document() 24 | assert.Equal(t, d.CharacterSet(), "UTF-8") 25 | } 26 | 27 | func TestDocumentContentType(t *testing.T) { 28 | d := GetWindow().Document() 29 | assert.Equal(t, d.ContentType(), "text/html") 30 | } 31 | 32 | func TestDocumentDocType(t *testing.T) { 33 | d := GetWindow().Document() 34 | assert.Equal(t, d.DocType(), "html") 35 | } 36 | 37 | func TestDocumentDomain(t *testing.T) { 38 | d := GetWindow().Document() 39 | assert.Equal(t, d.Domain(), "127.0.0.1") 40 | } 41 | 42 | func TestDocumentReferrer(t *testing.T) { 43 | d := GetWindow().Document() 44 | assert.Equal(t, d.Referrer(), "") 45 | } 46 | 47 | func TestDocumentReadyState(t *testing.T) { 48 | d := GetWindow().Document() 49 | assert.Equal(t, d.ReadyState(), "complete") 50 | } 51 | 52 | func TestDocumentTitle(t *testing.T) { 53 | d := GetWindow().Document() 54 | assert.Equal(t, d.Title(), "Go wasm") 55 | } 56 | 57 | func TestDocumentBody(t *testing.T) { 58 | d := GetWindow().Document() 59 | element := d.Body() 60 | assert.Equal(t, element.Type(), js.TypeObject) 61 | assert.Equal(t, element.Call("toString").String(), "[object HTMLBodyElement]") 62 | } 63 | 64 | func TestDocumentHead(t *testing.T) { 65 | d := GetWindow().Document() 66 | element := d.Head() 67 | assert.Equal(t, element.Type(), js.TypeObject) 68 | assert.Equal(t, element.Call("toString").String(), "[object HTMLHeadElement]") 69 | } 70 | 71 | func TestDocumentHTML(t *testing.T) { 72 | d := GetWindow().Document() 73 | element := d.HTML() 74 | assert.Equal(t, element.Type(), js.TypeObject) 75 | assert.Equal(t, element.Call("toString").String(), "[object HTMLHtmlElement]") 76 | } 77 | 78 | func TestDocumentDesignMode(t *testing.T) { 79 | d := GetWindow().Document() 80 | assert.False(t, d.DesignMode()) 81 | } 82 | 83 | func TestDocumentHidden(t *testing.T) { 84 | d := GetWindow().Document() 85 | assert.False(t, d.Hidden()) 86 | } 87 | 88 | func TestDocumentLastModified(t *testing.T) { 89 | d := GetWindow().Document() 90 | assert.WithinDuration(t, d.LastModified(), time.Now(), 5*time.Second) 91 | } 92 | 93 | func TestDocumentCreateNode(t *testing.T) { 94 | d := GetWindow().Document() 95 | bodyNode := d.Body().Node() 96 | assert.Equal(t, bodyNode.ChildrenCount(), 3) 97 | el := d.CreateElement("test") 98 | assert.Equal(t, bodyNode.ChildrenCount(), 3) 99 | bodyNode.AppendChild(el.Node()) 100 | assert.Equal(t, bodyNode.ChildrenCount(), 4) 101 | bodyNode.RemoveChild(el.Node()) 102 | assert.Equal(t, bodyNode.ChildrenCount(), 3) 103 | } 104 | -------------------------------------------------------------------------------- /web/element.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "syscall/js" 5 | ) 6 | 7 | type Element struct { 8 | Value 9 | } 10 | 11 | // SUBTYPES GETTERS 12 | 13 | func (el *Element) Attribute(name string) Attribute { 14 | return Attribute{value: el.Value, Namespace: "", Name: name} 15 | } 16 | 17 | func (el *Element) Class() Class { 18 | return Class{value: el.Value} 19 | } 20 | 21 | func (el *Element) Client() Client { 22 | return Client{value: el.Value} 23 | } 24 | 25 | func (el Element) Shadow() ShadowDOM { 26 | return ShadowDOM{value: el.Value} 27 | } 28 | 29 | func (el *Element) Scroll() Scroll { 30 | return Scroll{value: el.Value} 31 | } 32 | 33 | // SLOTS 34 | 35 | func (el Element) AssignedSlot() Element { 36 | return el.Get("assignedSlot").Element() 37 | } 38 | 39 | func (el Element) Slot() string { 40 | return el.Get("slot").OptionalString() 41 | } 42 | 43 | func (el Element) SetSlot(name string) { 44 | el.Set("slot", name) 45 | } 46 | 47 | // GETTERS 48 | 49 | func (el *Element) ID() string { 50 | return el.Get("id").String() 51 | } 52 | 53 | func (el Element) InnerHTML() string { 54 | return el.Get("innerHTML").String() 55 | } 56 | 57 | func (el *Element) LocalName() string { 58 | return el.Get("localName").String() 59 | } 60 | 61 | func (el *Element) OuterHTML() string { 62 | return el.Get("outerHTML").String() 63 | } 64 | 65 | func (el *Element) TagName() string { 66 | return el.Get("tagName").String() 67 | } 68 | 69 | // SETTERS 70 | 71 | func (el Element) SetID(id string) { 72 | el.Set("id", id) 73 | } 74 | 75 | func (el Element) SetInnerHTML(html string) { 76 | el.Set("innerHTML", html) 77 | } 78 | 79 | // POINTER METHODS 80 | 81 | func (el *Element) ReleasePointerCapture(pointerID string) { 82 | el.Call("releasePointerCapture", pointerID) 83 | } 84 | 85 | func (el *Element) RequestPointerLock() { 86 | el.Call("requestPointerLock") 87 | } 88 | 89 | func (el *Element) SetPointerCapture(pointerID string) { 90 | el.Call("setPointerCapture", pointerID) 91 | } 92 | 93 | // OTHER METHODS 94 | 95 | func (el *Element) Matches(selector string) bool { 96 | return el.Call("matches", selector).Bool() 97 | } 98 | 99 | func (el *Element) ScrollBy(x, y int, smooth bool) { 100 | if !smooth { 101 | el.Call("scrollBy", x, y) 102 | return 103 | } 104 | 105 | opts := js.Global().Get("Object").New() 106 | opts.Set("left", x) 107 | opts.Set("top", y) 108 | opts.Set("behavior", "smooth") 109 | el.Call("scrollBy", opts) 110 | } 111 | 112 | func (el *Element) ScrollTo(x, y int, smooth bool) { 113 | if !smooth { 114 | el.Call("scrollTo", x, y) 115 | return 116 | } 117 | 118 | opts := js.Global().Get("Object").New() 119 | opts.Set("left", x) 120 | opts.Set("top", y) 121 | opts.Set("behavior", "smooth") 122 | el.Call("scrollTo", opts) 123 | } 124 | 125 | func (el *Element) ScrollIntoView(smooth bool, block, inline string) { 126 | opts := js.Global().Get("Object").New() 127 | opts.Set("block", block) 128 | opts.Set("inline", inline) 129 | if smooth { 130 | opts.Set("behavior", "smooth") 131 | } else { 132 | opts.Set("behavior", "auto") 133 | } 134 | el.Call("scrollIntoView", opts) 135 | } 136 | 137 | // ELEMENT SUBTYPES 138 | 139 | type Attribute struct { 140 | value Value 141 | Namespace string 142 | Name string 143 | } 144 | 145 | func (attr *Attribute) Get() string { 146 | var v Value 147 | if attr.Namespace == "" { 148 | v = attr.value.Call("getAttribute", attr.Name) 149 | } else { 150 | v = attr.value.Call("getAttributeNS", attr.Namespace, attr.Name) 151 | } 152 | return v.OptionalString() 153 | } 154 | 155 | func (attr Attribute) Exists() bool { 156 | var v Value 157 | if attr.Namespace == "" { 158 | v = attr.value.Call("hasAttribute", attr.Name) 159 | } else { 160 | v = attr.value.Call("hasAttributeNS", attr.Namespace, attr.Name) 161 | } 162 | return v.Bool() 163 | } 164 | 165 | func (attr Attribute) Remove() { 166 | if attr.Namespace == "" { 167 | attr.value.Call("removeAttribute", attr.Name) 168 | } else { 169 | attr.value.Call("removeAttributeNS", attr.Namespace, attr.Name) 170 | } 171 | } 172 | 173 | func (attr Attribute) Set(value string) { 174 | if attr.Namespace == "" { 175 | attr.value.Call("setAttribute", attr.Name, value) 176 | } else { 177 | attr.value.Call("setAttributeNS", attr.Namespace, attr.Name, value) 178 | } 179 | } 180 | 181 | func (attr Attribute) Toggle() { 182 | attr.value.Call("toggleAttribute", attr.Name) 183 | } 184 | 185 | type Client struct { 186 | value Value 187 | } 188 | 189 | func (client Client) Height() int { 190 | return client.value.Get("clientHeight").Int() 191 | } 192 | 193 | func (client Client) Left() int { 194 | return client.value.Get("clientLeft").Int() 195 | } 196 | 197 | func (client Client) Top() int { 198 | return client.value.Get("clientTop").Int() 199 | } 200 | 201 | func (client Client) Width() int { 202 | return client.value.Get("clientWidth").Int() 203 | } 204 | 205 | type Scroll struct { 206 | value Value 207 | } 208 | 209 | func (scroll Scroll) Height() int { 210 | return scroll.value.Get("scrollHeight").Int() 211 | } 212 | 213 | func (scroll Scroll) Left() int { 214 | return scroll.value.Get("scrollLeft").Int() 215 | } 216 | 217 | func (scroll Scroll) Top() int { 218 | return scroll.value.Get("scrollTop").Int() 219 | } 220 | 221 | func (scroll Scroll) Width() int { 222 | return scroll.value.Get("scrollWidth").Int() 223 | } 224 | 225 | type ShadowDOM struct { 226 | value Value 227 | } 228 | 229 | // Attach attaches a shadow DOM tree to the specified element and returns ShadowRoot. 230 | // We always create "open" shadow DOM because "closed" can't totally 231 | // forbid access to the DOM and give falls feeling of protection. 232 | // Read more: https://blog.revillweb.com/open-vs-closed-shadow-dom-9f3d7427d1af 233 | func (shadow ShadowDOM) Attach() Element { 234 | opts := js.Global().Get("Object").New() 235 | opts.Set("mode", "open") 236 | return shadow.value.Call("attachShadow", opts).Element() 237 | } 238 | 239 | // Host returns a reference to the DOM element the ShadowRoot is attached to. 240 | func (shadow ShadowDOM) Host() Element { 241 | return shadow.value.Get("host").Element() 242 | } 243 | 244 | // Root returns ShadowRoot hosted by the element. 245 | func (shadow ShadowDOM) Root() Element { 246 | return shadow.value.Get("shadowRoot").Element() 247 | } 248 | 249 | type Class struct { 250 | value Value 251 | } 252 | 253 | // String returns `class` attribute 254 | func (cls Class) String() string { 255 | return cls.value.Get("className").String() 256 | } 257 | 258 | // Strings returns classes from `class` attribute 259 | func (cls Class) Strings() []string { 260 | v := cls.value.Get("classList") 261 | return v.Strings() 262 | } 263 | 264 | // Contains returns true if `class` attribute contains given class 265 | func (cls Class) Contains(name string) bool { 266 | return cls.value.Get("classList").Call("contains", name).Bool() 267 | } 268 | 269 | // Add adds new class into `class` attribute 270 | func (cls Class) Append(names ...string) { 271 | if len(names) == 0 { 272 | return 273 | } 274 | casted := make([]any, len(names)) 275 | for i, name := range names { 276 | casted[i] = any(name) 277 | } 278 | cls.value.Get("classList").Call("add", casted...) 279 | } 280 | 281 | // Remove removes class from classes list in `class` attribute 282 | func (cls Class) Remove(names ...string) { 283 | if len(names) == 0 { 284 | return 285 | } 286 | casted := make([]any, len(names)) 287 | for i, name := range names { 288 | casted[i] = any(name) 289 | } 290 | cls.value.Get("classList").Call("remove", casted...) 291 | } 292 | 293 | // Set overwrites the whole `class` attribute 294 | func (cls Class) Set(name string) { 295 | cls.value.Set("className", name) 296 | } 297 | -------------------------------------------------------------------------------- /web/element_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "syscall/js" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestElementAttribute(t *testing.T) { 11 | b := GetWindow().Document().Body() 12 | attr := b.Attribute("test") 13 | assert.False(t, attr.Exists()) 14 | 15 | attr.Set("val") 16 | assert.True(t, attr.Exists()) 17 | assert.Equal(t, attr.Get(), "val") 18 | 19 | attr.Remove() 20 | assert.False(t, attr.Exists()) 21 | } 22 | 23 | func TestElementClass(t *testing.T) { 24 | element := GetWindow().Document().CreateElement("lol") 25 | class := element.Class() 26 | 27 | assert.Equal(t, class.String(), "") 28 | class.Set("one two") 29 | assert.Equal(t, class.String(), "one two") 30 | 31 | assert.Equal(t, class.Strings(), []string{"one", "two"}) 32 | class.Append("three", "four") 33 | assert.Equal(t, class.Strings(), []string{"one", "two", "three", "four"}) 34 | class.Remove("two", "three") 35 | assert.Equal(t, class.Strings(), []string{"one", "four"}) 36 | assert.Equal(t, class.String(), "one four") 37 | 38 | assert.True(t, class.Contains("one")) 39 | assert.False(t, class.Contains("two")) 40 | } 41 | 42 | func TestElementClient(t *testing.T) { 43 | b := GetWindow().Document().Body() 44 | c := b.Client() 45 | assert.Equal(t, c.Width(), 784) 46 | assert.Equal(t, c.Height(), 0) 47 | assert.Equal(t, c.Left(), 0) 48 | assert.Equal(t, c.Top(), 0) 49 | } 50 | 51 | func TestElementScroll(t *testing.T) { 52 | b := GetWindow().Document().Body() 53 | s := b.Scroll() 54 | assert.Equal(t, s.Width(), 784) 55 | assert.Equal(t, s.Height(), 0) 56 | assert.Equal(t, s.Left(), 0) 57 | assert.Equal(t, s.Top(), 0) 58 | } 59 | 60 | func TestElementSlots(t *testing.T) { 61 | d := GetWindow().Document() 62 | body := d.Body() 63 | 64 | // create