├── .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 |
40 | {{ item['name'] }}
41 |
42 |
43 |
44 | source
45 |
46 |
47 |
48 |
49 | demo
50 |
51 |
52 |
53 | {% markdown %}
54 | {{ item['info'] }}
55 | {% endmarkdown %}
56 |
57 |
58 | {% endfor %}
59 |
60 |
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
65 | template := d.CreateElement("template")
66 | body.Node().AppendChild(template.Node())
67 |
68 | // add into template
69 | slot := d.CreateElement("slot")
70 | slot.Set("name", "example")
71 | slot.SetInnerHTML("default text")
72 | template.Node().AppendChild(slot.Node())
73 |
74 | // make element that will fill the
75 | span := d.CreateElement("span")
76 | assert.Equal(t, span.Slot(), "")
77 | span.SetSlot("example")
78 | assert.Equal(t, span.Slot(), "example")
79 | body.Node().AppendChild(span.Node())
80 |
81 | // render template
82 | shadow := body.Shadow().Attach()
83 | assert.Equal(t, span.AssignedSlot().Type(), js.TypeNull)
84 | shadow.Node().AppendChild(template.Node())
85 | assert.NotEqual(t, span.AssignedSlot().Type(), js.TypeNull)
86 | assert.Equal(t, span.AssignedSlot().InnerHTML(), "default text")
87 |
88 | // clean up
89 | assert.Equal(t, body.Node().ChildrenCount(), 4)
90 | span.Node().Remove()
91 | assert.Equal(t, body.Node().ChildrenCount(), 3)
92 | }
93 |
--------------------------------------------------------------------------------
/web/embed.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | // Embed represents HTMLEmbedElement and HTMLObjectElement.
4 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLEmbedElement
5 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement
6 | type Embed struct {
7 | HTMLElement
8 | }
9 |
10 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/height
11 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#attr-height
12 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed#attr-height
13 | func (embed *Embed) Height() int {
14 | return embed.Get("height").Int()
15 | }
16 |
17 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed#attr-src
18 | func (embed *Embed) Src() int {
19 | return embed.Get("src").Int()
20 | }
21 |
22 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/type
23 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#attr-type
24 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed#attr-type
25 | func (embed *Embed) MIMEType() string {
26 | return embed.Get("type").String()
27 | }
28 |
29 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/width
30 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#attr-width
31 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed#attr-width
32 | func (embed *Embed) Width() int {
33 | return embed.Get("Width").Int()
34 | }
35 |
--------------------------------------------------------------------------------
/web/event.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | // https://developer.mozilla.org/en-US/docs/Web/API/Event
4 | type Event struct {
5 | Value
6 | }
7 |
8 | // GETTERS
9 |
10 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles
11 | func (event *Event) Bubbles() bool {
12 | return event.Get("bubbles").Bool()
13 | }
14 |
15 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable
16 | func (event *Event) Cancelable() bool {
17 | return event.Get("cancelable").Bool()
18 | }
19 |
20 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
21 | func (event *Event) Composed() bool {
22 | return event.Get("composed").Bool()
23 | }
24 |
25 | func (event *Event) CurrentTarget() Value {
26 | return event.Get("currentTarget")
27 | }
28 |
29 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted
30 | func (event *Event) Trusted() bool {
31 | return event.Get("isTrusted").Bool()
32 | }
33 |
34 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/type
35 | func (event *Event) EventType() EventType {
36 | return EventType(event.Get("type").String())
37 | }
38 |
39 | // METHODS
40 |
41 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
42 | func (event *Event) PreventDefault() {
43 | event.Call("preventDefault")
44 | }
45 |
46 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation
47 | func (event *Event) StopImmediatePropagation() {
48 | event.Call("stopImmediatePropagation")
49 | }
50 |
51 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation
52 | func (event *Event) StopPropagation() {
53 | event.Call("stopPropagation")
54 | }
55 |
56 | // EVENT TYPES
57 | // https://developer.mozilla.org/en-US/docs/Web/Events
58 | type EventType string
59 |
60 | const (
61 | // EventTypeError means that a resource failed to load.
62 | EventTypeError = EventType("error")
63 |
64 | // EventTypeAbort means that the loading of a resource has been aborted.
65 | EventTypeAbort = EventType("abort")
66 |
67 | // EventTypeLoad means that a resource and its dependent resources have finished loading.
68 | EventTypeLoad = EventType("load")
69 |
70 | // EventTypeBeforeUnload means that the window, the document and its resources are about to be unloaded.
71 | EventTypeBeforeUnload = EventType("beforeunload")
72 |
73 | // EventTypeUnload means that the document or a dependent resource is being unloaded.
74 | EventTypeUnload = EventType("unload")
75 |
76 | // EventTypeOnline means that the browser has gained access to the network.
77 | EventTypeOnline = EventType("online")
78 |
79 | // EventTypeOffline means that the browser has lost access to the network.
80 | EventTypeOffline = EventType("offline")
81 |
82 | // EventTypeFocus means that an element has received focus (does not bubble).
83 | EventTypeFocus = EventType("focus")
84 |
85 | // EventTypeBlur means that an element has lost focus (does not bubble).
86 | EventTypeBlur = EventType("blur")
87 |
88 | // EventTypeOpen means that a WebSocket connection has been established.
89 | EventTypeOpen = EventType("open")
90 |
91 | // EventTypeMessage means that a message is received through a WebSocket.
92 | EventTypeMessage = EventType("message")
93 |
94 | // EventTypeClose means that a WebSocket connection has been closed.
95 | EventTypeClose = EventType("close")
96 |
97 | // EventTypePageHide means that a session history entry is being traversed from.
98 | EventTypePageHide = EventType("pagehide")
99 |
100 | // EventTypePageShow means that a session history entry is being traversed to.
101 | EventTypePageShow = EventType("pageshow")
102 |
103 | // EventTypePopState means that a session history entry is being navigated to (in certain cases).
104 | EventTypePopState = EventType("popstate")
105 |
106 | // EventTypeAnimationStart means that a CSS animation has started.
107 | EventTypeAnimationStart = EventType("animationstart")
108 |
109 | // EventTypeAnimationCancel means that a CSS animation has aborted.
110 | EventTypeAnimationCancel = EventType("animationcancel")
111 |
112 | // EventTypeAnimationEnd means that a CSS animation has completed.
113 | EventTypeAnimationEnd = EventType("animationend")
114 |
115 | // EventTypeAnimationiteration means that a CSS animation is repeated.
116 | EventTypeAnimationIteration = EventType("animationiteration")
117 |
118 | // EventTypeTransitionStart means that a CSS transition has actually started (fired after any delay).
119 | EventTypeTransitionStart = EventType("transitionstart")
120 |
121 | // EventTypeTransitionCancel means that a CSS transition has been cancelled.
122 | EventTypeTransitionCancel = EventType("transitioncancel")
123 |
124 | // EventTypeTransitionEnd means that a CSS transition has completed.
125 | EventTypeTransitionEnd = EventType("transitionend")
126 |
127 | // EventTypeTransitionRun means that a CSS transition has begun running (fired before any delay starts).
128 | EventTypeTransitionRun = EventType("transitionrun")
129 |
130 | // EventTypeReset means that the reset button is pressed.
131 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset_event
132 | EventTypeReset = EventType("reset")
133 |
134 | // EventTypeSubmit means that the submit button is pressed.
135 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event
136 | EventTypeSubmit = EventType("submit")
137 |
138 | // EventTypeBeforePrint means that the print dialog is opened.
139 | EventTypeBeforePrint = EventType("beforeprint")
140 |
141 | // EventTypeAfterPrint means that the print dialog is closed.
142 | EventTypeAfterPrint = EventType("afterprint")
143 |
144 | // EventTypeCompositionStart means that the composition of a passage of text is prepared (similar to keydown for a keyboard input, but works with other inputs such as speech recognition).
145 | EventTypeCompositionStart = EventType("compositionstart")
146 |
147 | // EventTypeCompositionUpdate means that a character is added to a passage of text being composed.
148 | EventTypeCompositionUpdate = EventType("compositionupdate")
149 |
150 | // EventTypeCompositionEnd means that the composition of a passage of text has been completed or canceled.
151 | EventTypeCompositionEnd = EventType("compositionend")
152 |
153 | // EventTypeFullscreenChange means that an element was turned to fullscreen mode or back to normal mode.
154 | EventTypeFullscreenChange = EventType("fullscreenchange")
155 |
156 | // EventTypeFullscreenError means that It was impossible to switch to fullscreen mode for technical reasons or because the permission was denied.
157 | EventTypeFullscreenError = EventType("fullscreenerror")
158 |
159 | // EventTypeResize means that the document view has been resized.
160 | EventTypeResize = EventType("resize")
161 |
162 | // EventTypeScroll means that the document view or an element has been scrolled.
163 | EventTypeScroll = EventType("scroll")
164 |
165 | // EventTypeCut means that the selection has been cut and copied to the clipboard
166 | EventTypeCut = EventType("cut")
167 |
168 | // EventTypeCopy means that the selection has been copied to the clipboard
169 | EventTypeCopy = EventType("copy")
170 |
171 | // EventTypePaste means that the item from the clipboard has been pasted
172 | EventTypePaste = EventType("paste")
173 |
174 | // EventTypeKeyDown means that aNY key is pressed
175 | EventTypeKeyDown = EventType("keydown")
176 |
177 | // EventTypeKeyPress means that aNY key except Shift, Fn, CapsLock is in pressed position. (Fired continously.)
178 | EventTypeKeyPress = EventType("keypress")
179 |
180 | // EventTypeKeyUp means that aNY key is released
181 | EventTypeKeyUp = EventType("keyup")
182 |
183 | // EventTypeAuxClick means that a pointing device button (ANY non-primary button) has been pressed and released on an element.
184 | EventTypeAuxClick = EventType("auxclick")
185 |
186 | // EventTypeClick means that a pointing device button (ANY button; soon to be primary button only) has been pressed and released on an element.
187 | EventTypeClick = EventType("click")
188 |
189 | // EventTypeContextMenu means that the right button of the mouse is clicked (before the context menu is displayed).
190 | EventTypeContextMenu = EventType("contextmenu")
191 |
192 | // EventTypeDoubleClick means that a pointing device button is clicked twice on an element.
193 | EventTypeDoubleClick = EventType("dblclick")
194 |
195 | // EventTypeMouseDown means that a pointing device button is pressed on an element.
196 | EventTypeMouseDown = EventType("mousedown")
197 |
198 | // EventTypeMouseEnter means that a pointing device is moved onto the element that has the listener attached.
199 | EventTypeMouseEnter = EventType("mouseenter")
200 |
201 | // EventTypeMouseLeave means that a pointing device is moved off the element that has the listener attached.
202 | EventTypeMouseLeave = EventType("mouseleave")
203 |
204 | // EventTypeMouseMove means that a pointing device is moved over an element. (Fired continously as the mouse moves.)
205 | EventTypeMouseMove = EventType("mousemove")
206 |
207 | // EventTypeMouseOver means that a pointing device is moved onto the element that has the listener attached or onto one of its children.
208 | EventTypeMouseOver = EventType("mouseover")
209 |
210 | // EventTypeMouseOut means that a pointing device is moved off the element that has the listener attached or off one of its children.
211 | EventTypeMouseOut = EventType("mouseout")
212 |
213 | // EventTypeMouseUp means that a pointing device button is released over an element.
214 | EventTypeMouseUp = EventType("mouseup")
215 |
216 | // EventTypePointerLockChange means that the pointer was locked or released.
217 | EventTypePointerLockChange = EventType("pointerlockchange")
218 |
219 | // EventTypePointerLockError means that It was impossible to lock the pointer for technical reasons or because the permission was denied.
220 | EventTypePointerLockError = EventType("pointerlockerror")
221 |
222 | // EventTypeSelect means that Some text is being selected.
223 | EventTypeSelect = EventType("select")
224 |
225 | // EventTypeWheel means that a wheel button of a pointing device is rotated in any direction.
226 | EventTypeWheel = EventType("wheel")
227 |
228 | // EventTypeDrag means that an element or text selection is being dragged (Fired continuously every 350ms).
229 | EventTypeDrag = EventType("drag")
230 |
231 | // EventTypeDragEnd means that a drag operation is being ended (by releasing a mouse button or hitting the escape key).
232 | EventTypeDragEnd = EventType("dragend")
233 |
234 | // EventTypeDragEnter means that a dragged element or text selection enters a valid drop target.
235 | EventTypeDragEnter = EventType("dragenter")
236 |
237 | // EventTypeDragStart means that the user starts dragging an element or text selection.
238 | EventTypeDragStart = EventType("dragstart")
239 |
240 | // EventTypeDragLeave means that a dragged element or text selection leaves a valid drop target.
241 | EventTypeDragLeave = EventType("dragleave")
242 |
243 | // EventTypeDragOver means that an element or text selection is being dragged over a valid drop target. (Fired continuously every 350ms.)
244 | EventTypeDragOver = EventType("dragover")
245 |
246 | // EventTypeDrop means that an element is dropped on a valid drop target.
247 | EventTypeDrop = EventType("drop")
248 |
249 | // EventTypeAudioProcess means that the input buffer of a ScriptProcessorNode is ready to be processed.
250 | EventTypeAudioProcess = EventType("audioprocess")
251 |
252 | // EventTypeCanPlay means that the browser can play the media, but estimates that not enough data has been loaded to play the media up to its end without having to stop for further buffering of content.
253 | EventTypeCanPlay = EventType("canplay")
254 |
255 | // EventTypeCanPlayThrough means that the browser estimates it can play the media up to its end without stopping for content buffering.
256 | EventTypeCanPlayThrough = EventType("canplaythrough")
257 |
258 | // EventTypeComplete means that the rendering of an OfflineAudioContext is terminated.
259 | EventTypeComplete = EventType("complete")
260 |
261 | // EventTypeDurationChange means that the duration attribute has been updated.
262 | EventTypeDurationChange = EventType("durationchange")
263 |
264 | // EventTypeEmptied means that the media has become empty; for example, this event is sent if the media has already been loaded (or partially loaded), and the load() method is called to reload it.
265 | EventTypeEmptied = EventType("emptied")
266 |
267 | // EventTypeEnded means that Playback has stopped because the end of the media was reached.
268 | EventTypeEnded = EventType("ended")
269 |
270 | // EventTypeLoadedData means that the first frame of the media has finished loading.
271 | EventTypeLoadedData = EventType("loadeddata")
272 |
273 | // EventTypeLoadedMetadata means that the metadata has been loaded.
274 | EventTypeLoadedMetadata = EventType("loadedmetadata")
275 |
276 | // EventTypePause means that Playback has been paused.
277 | EventTypePause = EventType("pause")
278 |
279 | // EventTypePlay means that Playback has begun.
280 | EventTypePlay = EventType("play")
281 |
282 | // EventTypePlaying means that Playback is ready to start after having been paused or delayed due to lack of data.
283 | EventTypePlaying = EventType("playing")
284 |
285 | // EventTypeRateChange means that the playback rate has changed.
286 | EventTypeRateChange = EventType("ratechange")
287 |
288 | // EventTypeSeeked means that a seek operation completed.
289 | EventTypeSeeked = EventType("seeked")
290 |
291 | // EventTypeSeeking means that a seek operation began.
292 | EventTypeSeeking = EventType("seeking")
293 |
294 | // EventTypeStalled means that the user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
295 | EventTypeStalled = EventType("stalled")
296 |
297 | // EventTypeSuspend means that Media data loading has been suspended.
298 | EventTypeSuspend = EventType("suspend")
299 |
300 | // EventTypeTimeUpdate means that the time indicated by the currentTime attribute has been updated.
301 | EventTypeTimeUpdate = EventType("timeupdate")
302 |
303 | // EventTypeVolumeChange means that the volume has changed.
304 | EventTypeVolumeChange = EventType("volumechange")
305 |
306 | // EventTypeWaiting means that Playback has stopped because of a temporary lack of data.
307 | EventTypeWaiting = EventType("waiting")
308 |
309 | // EventTypeLoadEnd means that Progress has stopped (after "error", "abort" or "load" have been dispatched).
310 | EventTypeLoadEnd = EventType("loadend")
311 |
312 | // EventTypeLoadStart means that Progress has begun.
313 | EventTypeLoadStart = EventType("loadstart")
314 |
315 | // EventTypeProgress means that In progress.
316 | EventTypeProgress = EventType("progress")
317 |
318 | // EventTypeTimeout means that Progression is terminated due to preset time expiring.
319 | EventTypeTimeout = EventType("timeout")
320 | )
321 |
--------------------------------------------------------------------------------
/web/event_target.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "syscall/js"
5 | )
6 |
7 | // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
8 | type EventTarget struct {
9 | Value
10 | }
11 |
12 | // Listen registers callback for the given event.
13 | // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
14 | func (target EventTarget) Listen(event EventType, handler func(event Event)) {
15 | wrapped := func(this js.Value, args []js.Value) any {
16 | v := Value{Value: args[0]}
17 | handler(v.Event())
18 | return nil
19 | }
20 | target.Call("addEventListener", string(event), js.FuncOf(wrapped))
21 | }
22 |
--------------------------------------------------------------------------------
/web/html_element.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "github.com/life4/gweb/css"
5 | )
6 |
7 | type Editable string
8 |
9 | const (
10 | EditableTrue = Editable("true")
11 | EditableFalse = Editable("false")
12 | EditableInherit = Editable("inherit")
13 | )
14 |
15 | type Direction string
16 |
17 | const (
18 | DirectionLTR = Direction("ltr")
19 | DirectionRTL = Direction("rtl")
20 | DirectionAuto = Direction("auto")
21 | )
22 |
23 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
24 | type HTMLElement struct {
25 | Element
26 | }
27 |
28 | // SUBTYPES GETTERS
29 |
30 | // Incapsulates a set of offset-related properties.
31 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
32 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft
33 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
34 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop
35 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
36 | func (el *HTMLElement) Offset() Offset {
37 | return Offset{value: el.Value}
38 | }
39 |
40 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
41 | func (el *HTMLElement) Style() css.CSSStyleDeclaration {
42 | value := el.Get("style").JSValue()
43 | return css.CSSStyleDeclaration{Value: value}
44 | }
45 |
46 | // GETTERS
47 |
48 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dir
49 | func (el *HTMLElement) Direction() Direction {
50 | return Direction(el.Get("dir").String())
51 | }
52 |
53 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/isContentEditable
54 | func (el *HTMLElement) Editable() bool {
55 | return el.Get("isContentEditable").Bool()
56 | }
57 |
58 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/hidden
59 | func (el *HTMLElement) Hidden() bool {
60 | return el.Get("hidden").Bool()
61 | }
62 |
63 | func (el *HTMLElement) Lang() string {
64 | return el.Get("lang").String()
65 | }
66 |
67 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/nonce
68 | func (el *HTMLElement) Nonce() string {
69 | return el.Get("nonce").String()
70 | }
71 |
72 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
73 | func (el *HTMLElement) Text() string {
74 | return el.Get("innerText").String()
75 | }
76 |
77 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/tabIndex
78 | func (el *HTMLElement) TabIndex() int {
79 | return el.Get("tabIndex").Int()
80 | }
81 |
82 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/title
83 | func (el *HTMLElement) Title() string {
84 | return el.Get("title").String()
85 | }
86 |
87 | // SETTERS
88 |
89 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dir
90 | func (el HTMLElement) SetDirection(value Direction) {
91 | el.Set("dir", string(value))
92 | }
93 |
94 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/contentEditable
95 | func (el HTMLElement) SetEditable(value Editable) {
96 | el.Set("contentEditable", string(value))
97 | }
98 |
99 | func (el HTMLElement) SetHidden(value bool) {
100 | el.Set("hidden", value)
101 | }
102 |
103 | func (el HTMLElement) SetLang(value string) {
104 | el.Set("lang", value)
105 | }
106 |
107 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
108 | func (el HTMLElement) SetText(text string) {
109 | el.Set("innerText", text)
110 | }
111 |
112 | // HTMLElement SUBTYPES
113 |
114 | // Incapsulates a set of offset-related properties.
115 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
116 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft
117 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
118 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop
119 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
120 | type Offset struct {
121 | value Value
122 | }
123 |
124 | // Returns the height of an element, relative to the layout.
125 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
126 | func (offset *Offset) Height() int {
127 | return offset.value.Get("offsetHeight").Int()
128 | }
129 |
130 | // Returns the distance from this element's left border to its Offset.Parent's left border.
131 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft
132 | func (offset *Offset) Left() int {
133 | return offset.value.Get("offsetLeft").Int()
134 | }
135 |
136 | // Returns the distance from this element's top border to its Offset.Parent's top border.
137 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop
138 | func (offset *Offset) Top() int {
139 | return offset.value.Get("offsetTop").Int()
140 | }
141 |
142 | // Returns the width of an element, relative to the layout.
143 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
144 | func (offset *Offset) Width() int {
145 | return offset.value.Get("offsetWidth").Int()
146 | }
147 |
148 | // Returns an Element that is the element
149 | // from which all offset calculations are currently computed.
150 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
151 | func (offset *Offset) Parent() Element {
152 | v := offset.value.Get("offsetParent")
153 | return Element{Value: v}
154 | }
155 |
--------------------------------------------------------------------------------
/web/http_request.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | "syscall/js"
7 | "time"
8 | )
9 |
10 | // Object used to send HTTP requests.
11 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
12 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/XMLHttpRequest
13 | type HTTPRequest struct {
14 | Value
15 | window Window
16 | }
17 |
18 | // Send the HTTP request. This operation is blocking on the Go side
19 | // but doesn't block JS-side main thread.
20 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send
21 | func (req HTTPRequest) Send(body []byte) HTTPResponse {
22 | wg := sync.WaitGroup{}
23 | wg.Add(1)
24 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/onreadystatechange
25 | req.Set("onreadystatechange", js.FuncOf(func(this js.Value, args []js.Value) any {
26 | state := req.Get("readyState").Int()
27 | if state == 4 || state == 0 {
28 | wg.Done()
29 | }
30 | return nil
31 | }))
32 |
33 | if body == nil {
34 | req.Call("send", nil)
35 | } else {
36 | encoded := req.window.Get("Uint8Array").New(len(body))
37 | js.CopyBytesToJS(encoded.Value, body)
38 | req.Call("send", encoded)
39 | }
40 |
41 | wg.Wait()
42 | return HTTPResponse{
43 | value: req.Value,
44 | window: req.window,
45 | }
46 | }
47 |
48 | // Abort aborts the request if it has already been sent.
49 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
50 | func (req HTTPRequest) Abort() {
51 | req.Call("abort")
52 | }
53 |
54 | // Timeout represents how long a request can take before automatically being terminated.
55 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
56 | func (req HTTPRequest) Timeout() time.Duration {
57 | return time.Duration(req.Get("timeout").Int()) * time.Millisecond
58 | }
59 |
60 | // SetTimeout sets the time after which the request will be terminated.
61 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
62 | func (req HTTPRequest) SetTimeout(timeout time.Duration) {
63 | req.Set("timeout", int(timeout/time.Millisecond))
64 | }
65 |
66 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
67 | func (req HTTPRequest) WithCredentials() bool {
68 | return req.Get("withCredentials").Bool()
69 | }
70 |
71 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
72 | func (req HTTPRequest) SetWithCredentials(creds bool) {
73 | req.Set("withCredentials", creds)
74 | }
75 |
76 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
77 | func (req HTTPRequest) SetHeader(header, value string) {
78 | req.Call("setRequestHeader", header, value)
79 | }
80 |
81 | type HTTPResponse struct {
82 | value Value
83 | window Window
84 | }
85 |
86 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
87 | func (resp HTTPResponse) Body() []byte {
88 | raw := resp.value.Get("response")
89 | if raw.IsNull() {
90 | return nil
91 | }
92 | raw = resp.window.Get("Uint8Array").New(raw)
93 | dec := make([]byte, raw.Length())
94 | js.CopyBytesToGo(dec, raw.Value)
95 | return dec
96 | }
97 |
98 | // Finished indicates is the request is succesfully completed.
99 | // It can be false if the request was aborted.
100 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
101 | func (resp HTTPResponse) Finished() bool {
102 | return resp.value.Get("readyState").Int() == 4
103 | }
104 |
105 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText
106 | func (resp HTTPResponse) Text() string {
107 | return resp.value.Get("responseText").OptionalString()
108 | }
109 |
110 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseURL
111 | func (resp HTTPResponse) URL() string {
112 | return resp.value.Get("responseURL").String()
113 | }
114 |
115 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status
116 | func (resp HTTPResponse) StatusCode() int {
117 | return resp.value.Get("status").Int()
118 | }
119 |
120 | // Always an empty string for HTTP/2 responses.
121 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/statusText
122 | func (resp HTTPResponse) Status() string {
123 | return resp.value.Get("statusText").String()
124 | }
125 |
126 | func (resp HTTPResponse) Headers() Headers {
127 | return Headers{value: resp.value}
128 | }
129 |
130 | // Headers encapsulates methods to work with HTTP response headers.
131 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
132 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
133 | type Headers struct {
134 | value Value
135 | }
136 |
137 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
138 | func (h Headers) Get(name string) string {
139 | return h.value.Call("getResponseHeader", name).OptionalString()
140 | }
141 |
142 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
143 | func (h Headers) Has(name string) bool {
144 | return !h.value.Call("getResponseHeader", name).IsNull()
145 | }
146 |
147 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
148 | func (h Headers) Values() []string {
149 | vals := h.value.Call("getAllResponseHeaders").String()
150 | return strings.Split(strings.TrimSpace(vals), "\r\n")
151 | }
152 |
--------------------------------------------------------------------------------
/web/http_request_test.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestHTTPRequest_GET(t *testing.T) {
11 | is := require.New(t)
12 | req := GetWindow().HTTPRequest("GET", "https://httpbin.org/get")
13 | resp := req.Send(nil)
14 | is.Equal(resp.StatusCode(), 200)
15 | is.Equal(resp.Status(), "")
16 | is.Equal(resp.Headers().Get("Content-Type"), "application/json")
17 | is.Equal(len(resp.Headers().Values()), 2)
18 | }
19 |
20 | func TestHTTPRequest_POST(t *testing.T) {
21 | is := require.New(t)
22 | req := GetWindow().HTTPRequest("POST", "https://httpbin.org/post")
23 | resp := req.Send([]byte("hello world"))
24 | is.Equal(resp.StatusCode(), 200)
25 | is.Equal(resp.Status(), "")
26 | is.Equal(resp.Headers().Get("Content-Type"), "application/json")
27 | is.Equal(len(resp.Headers().Values()), 2)
28 |
29 | data := struct {
30 | Data string `json:"data"`
31 | URL string `json:"url"`
32 | }{}
33 | // is.Equal(string(resp.Body()), "")
34 | err := json.Unmarshal(resp.Body(), &data)
35 | is.Nil(err)
36 | is.Equal(data.URL, "https://httpbin.org/post")
37 | is.Equal(data.Data, "hello world")
38 | }
39 |
--------------------------------------------------------------------------------
/web/media_devices.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | type MediaDevices struct {
4 | Value
5 | }
6 |
7 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
8 | func (devices MediaDevices) Audio() Promise {
9 | params := map[string]any{"audio": true}
10 | return devices.Call("getUserMedia", params).Promise()
11 | }
12 |
13 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
14 | func (devices MediaDevices) Video() Promise {
15 | params := map[string]any{"video": true}
16 | return devices.Call("getUserMedia", params).Promise()
17 | }
18 |
--------------------------------------------------------------------------------
/web/navigator.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | type Navigator struct {
4 | Value
5 | }
6 |
7 | // PROPERTIES
8 |
9 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/cookieEnabled
10 | func (nav Navigator) CookieEnabled() bool {
11 | return nav.Get("cookieEnabled").Bool()
12 | }
13 |
14 | // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/language
15 | func (nav Navigator) Language() string {
16 | return nav.Get("language").String()
17 | }
18 |
19 | // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages
20 | func (nav Navigator) Languages() []string {
21 | return nav.Get("languages").Strings()
22 | }
23 |
24 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints
25 | func (nav Navigator) MaxTouchPoints() int {
26 | return nav.Get("maxTouchPoints").Int()
27 | }
28 |
29 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices
30 | func (nav Navigator) MediaDevices() MediaDevices {
31 | return MediaDevices{Value: nav.Get("mediaDevices")}
32 | }
33 |
34 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine
35 | func (nav Navigator) Online() bool {
36 | return nav.Get("onLine").Bool()
37 | }
38 |
39 | // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/userAgent
40 | func (nav Navigator) UserAgent() string {
41 | return nav.Get("userAgent").String()
42 | }
43 |
44 | // METHODS
45 |
46 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate
47 | func (nav Navigator) Vibrate(pattern []int) {
48 | nav.Call("vibrate", pattern)
49 | }
50 |
--------------------------------------------------------------------------------
/web/node.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import "syscall/js"
4 |
5 | type NodeType int
6 |
7 | const (
8 | ELEMENT_NODE = NodeType(1)
9 | ATTRIBUTE_NODE = NodeType(2) // deprecated
10 | TEXT_NODE = NodeType(3)
11 | CDATA_SECTION_NODE = NodeType(4)
12 | ENTITY_REFERENCE_NODE = NodeType(5) // deprecated
13 | ENTITY_NODE = NodeType(6) // deprecated
14 | PROCESSING_INSTRUCTION_NODE = NodeType(7)
15 | COMMENT_NODE = NodeType(8)
16 | DOCUMENT_NODE = NodeType(9)
17 | DOCUMENT_TYPE_NODE = NodeType(10)
18 | DOCUMENT_FRAGMENT_NODE = NodeType(11)
19 | NOTATION_NODE = NodeType(12) // deprecated
20 | )
21 |
22 | // https://developer.mozilla.org/en-US/docs/Web/API/Node
23 | // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode
24 | type Node struct {
25 | value Value
26 | }
27 |
28 | // PROPERTIES
29 |
30 | // Returns the base URL of the document containing the Node.
31 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI
32 | func (node Node) BaseURI() string {
33 | return node.value.Get("baseURI").String()
34 | }
35 |
36 | // Returns the number of children of this Node which are elements.
37 | // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/childElementCount
38 | func (node Node) ChildrenCount() int {
39 | return node.value.Get("childElementCount").Int()
40 | }
41 |
42 | // A boolean indicating whether or not the Node is connected
43 | // (directly or indirectly) to the context object, e.g.
44 | // the Document object in the case of the normal DOM,
45 | // or the ShadowRoot in the case of a shadow DOM.
46 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected
47 | func (node Node) Connected() bool {
48 | return node.value.Get("isConnected").Bool()
49 | }
50 |
51 | func (node Node) Content() string {
52 | return node.value.Get("textContent").String()
53 | }
54 |
55 | func (node Node) Document() Document {
56 | value := node.value.Get("ownerDocument")
57 | switch value.Type() {
58 | case js.TypeNull:
59 | return Document{Value: node.value}
60 | default:
61 | return Document{Value: value}
62 | }
63 | }
64 |
65 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName
66 | func (node Node) Name() string {
67 | return node.value.Get("nodeName").String()
68 | }
69 |
70 | // Returns the type of the node.
71 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
72 | func (node Node) Type() NodeType {
73 | return NodeType(node.value.Get("nodeType").Int())
74 | }
75 |
76 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeValue
77 | func (node Node) Value() string {
78 | return node.value.Get("nodeValue").OptionalString()
79 | }
80 |
81 | // METHODS
82 |
83 | // Clean up all the text nodes under this element (merge adjacent, remove empty).
84 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
85 | func (node Node) Normalize() {
86 | node.value.Call("normalize")
87 | }
88 |
89 | // Clone a Node, and optionally, all of its contents.
90 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode
91 | func (node Node) Clone(deep bool) Node {
92 | return node.value.Call("cloneNode", deep).Node()
93 | }
94 |
95 | // TREE
96 |
97 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
98 | func (node Node) AppendChild(child Node) {
99 | node.value.Call("appendChild", child.value)
100 | }
101 |
102 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes
103 | func (node Node) ChildNodes() []HTMLElement {
104 | nodes := node.value.Get("childNodes")
105 | values := nodes.Values()
106 | elements := make([]HTMLElement, len(values))
107 | for i, value := range values {
108 | elements[i] = value.HTMLElement()
109 | }
110 | return elements
111 | }
112 |
113 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/firstChild
114 | func (node Node) FirstChild() HTMLElement {
115 | return node.value.Get("firstChild").HTMLElement()
116 | }
117 |
118 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/hasChildNodes
119 | func (node Node) HasChildNodes() bool {
120 | return node.value.Call("hasChildNodes").Bool()
121 | }
122 |
123 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement
124 | func (node Node) Parent() HTMLElement {
125 | return node.value.Get("parentElement").HTMLElement()
126 | }
127 |
128 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
129 | func (node Node) RemoveChild(child Node) {
130 | node.value.Call("removeChild", child.value)
131 | }
132 |
133 | // Remove all children.
134 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
135 | func (node Node) RemoveChildren() {
136 | for {
137 | child := node.FirstChild()
138 | if child.Type() == js.TypeNull {
139 | return
140 | }
141 | node.value.Call("removeChild", child.Value)
142 | }
143 | }
144 |
145 | // Remove the node from the parent node.
146 | func (node Node) Remove() bool {
147 | parent := node.Parent()
148 | if parent.Type() == js.TypeNull {
149 | return false
150 | }
151 | parent.Call("removeChild", node.value)
152 | return true
153 | }
154 |
--------------------------------------------------------------------------------
/web/promise.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "sync"
5 | "syscall/js"
6 | )
7 |
8 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
9 | type Promise struct {
10 | Value
11 | }
12 |
13 | // Register callback for error handling
14 | func (promise Promise) Catch(handler func(reason js.Value)) {
15 | wrapper := func(then js.Value, args []js.Value) any {
16 | handler(args[0])
17 | return nil
18 | }
19 | promise.Call("catch", js.FuncOf(wrapper))
20 | }
21 |
22 | // Register callback for sucsessful result handling
23 | func (promise Promise) Then(handler func(value js.Value)) {
24 | wrapper := func(then js.Value, args []js.Value) any {
25 | handler(args[0])
26 | return nil
27 | }
28 | promise.Call("then", js.FuncOf(wrapper))
29 | }
30 |
31 | // Blocking call that returns values of sucsess and error
32 | func (promise Promise) Get() (msg Value, err Value) {
33 | // we'll wait only for one (the first) handler
34 | wg := sync.WaitGroup{}
35 | wg.Add(1)
36 |
37 | // register error handler
38 | catch := func(value js.Value) {
39 | err = Value{Value: value}
40 | wg.Done()
41 | }
42 | promise.Catch(catch)
43 |
44 | // register succsess handler
45 | then := func(reason js.Value) {
46 | msg = Value{Value: reason}
47 | wg.Done()
48 | }
49 | promise.Then(then)
50 |
51 | // wait until any handler is done
52 | wg.Wait()
53 | return msg, err
54 | }
55 |
--------------------------------------------------------------------------------
/web/screen.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen
4 | type Screen struct {
5 | Value
6 | }
7 |
8 | // PROPERTIES
9 |
10 | // Returns the height of the screen, in pixels,
11 | // minus permanent or semipermanent user interface features
12 | // displayed by the operating system, such as the Taskbar on Windows.
13 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen/availHeight
14 | func (screen Screen) AvailableHeight() int {
15 | return screen.Get("availHeight").Int()
16 | }
17 |
18 | // Returns the amount of horizontal space in pixels available to the window.
19 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen/availWidth
20 | func (screen Screen) AvailableWidth() int {
21 | return screen.Get("availWidth").Int()
22 | }
23 |
24 | // Returns the height of the screen in pixels.
25 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen/height
26 | func (screen Screen) Height() int {
27 | return screen.Get("height").Int()
28 | }
29 |
30 | // Returns the width of the screen.
31 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen/width
32 | func (screen Screen) Width() int {
33 | return screen.Get("width").Int()
34 | }
35 |
--------------------------------------------------------------------------------
/web/value.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "syscall/js"
5 |
6 | "github.com/life4/gweb/audio"
7 | )
8 |
9 | // Value is an extended js.Value with more types support
10 | type Value struct {
11 | js.Value
12 | }
13 |
14 | // overloaded methods
15 |
16 | // Call calls the given method of the object.
17 | func (v Value) Call(method string, args ...any) Value {
18 | result := v.Value.Call(method, unwrapValues(args)...)
19 | return Value{Value: result}
20 | }
21 |
22 | // Get returns the given property of the value
23 | func (v Value) Get(property string) Value {
24 | result := v.Value.Get(property)
25 | return Value{Value: result}
26 | }
27 |
28 | // Creates new instance of the JS class.
29 | func (v Value) New(args ...any) Value {
30 | result := v.Value.New(unwrapValues(args)...)
31 | return Value{Value: result}
32 | }
33 |
34 | // new methods
35 |
36 | // Casts web.Value to js.Value
37 | func (v Value) JSValue() js.Value {
38 | return v.Value
39 | }
40 |
41 | // Represents the current value into Canvas
42 | func (v Value) Canvas() Canvas {
43 | return Canvas{HTMLElement: v.HTMLElement()}
44 | }
45 |
46 | // Represents the current value into Element
47 | func (v Value) Element() Element {
48 | return Element{Value: v}
49 | }
50 |
51 | // Represents the current value into Embed
52 | func (v Value) Embed() Embed {
53 | return Embed{HTMLElement: v.HTMLElement()}
54 | }
55 |
56 | // Represents the current value into Event
57 | func (v Value) Event() Event {
58 | return Event{Value: v}
59 | }
60 |
61 | // Represents the current value into EventTarget
62 | func (v Value) EventTarget() EventTarget {
63 | return EventTarget{Value: v}
64 | }
65 |
66 | // Represents the current value into HTMLElement
67 | func (v Value) HTMLElement() HTMLElement {
68 | return HTMLElement{Element: v.Element()}
69 | }
70 |
71 | // Represents the current value into audio.MediaStream
72 | func (v Value) MediaStream() audio.MediaStream {
73 | return audio.MediaStream{Value: v.Value}
74 | }
75 |
76 | // Represents the current value into Node
77 | func (v Value) Node() Node {
78 | return Node{value: v}
79 | }
80 |
81 | // Represents the current value into Promise
82 | func (v Value) Promise() Promise {
83 | return Promise{Value: v}
84 | }
85 |
86 | // Represents the current value as slice of values
87 | func (v *Value) Values() (items []Value) {
88 | len := v.Get("length").Int()
89 | for i := 0; i < len; i++ {
90 | item := v.Call("item", i)
91 | items = append(items, item)
92 | }
93 | return items
94 | }
95 |
96 | // Represents the current value as slice of strings
97 | func (v Value) Strings() (items []string) {
98 | len := v.Get("length").Int()
99 | for i := 0; i < len; i++ {
100 | item := v.Call("item", i)
101 | items = append(items, item.String())
102 | }
103 | return items
104 | }
105 |
106 | // OptionalString returns empty string if Value is null
107 | func (v Value) OptionalString() string {
108 | switch v.Type() {
109 | case js.TypeNull:
110 | return ""
111 | case js.TypeString:
112 | return v.String()
113 | default:
114 | panic("bad type")
115 | }
116 | }
117 |
118 | func unwrapValues(args []any) []any {
119 | values := make([]any, len(args))
120 | for i, arg := range args {
121 | switch arg := arg.(type) {
122 | case Value:
123 | values[i] = arg.Value
124 | default:
125 | values[i] = arg
126 | }
127 | }
128 | return values
129 | }
130 |
--------------------------------------------------------------------------------
/web/window.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "syscall/js"
5 |
6 | "github.com/life4/gweb/audio"
7 | )
8 |
9 | // https://developer.mozilla.org/en-US/docs/Web/API/Window
10 | type Window struct {
11 | Value
12 | }
13 |
14 | // Returns JS global
15 | // https://developer.mozilla.org/en-US/docs/Web/API/Window
16 | func GetWindow() Window {
17 | value := Value{Value: js.Global()}
18 | return Window{Value: value}
19 | }
20 |
21 | // CONSTRUCTORS
22 |
23 | // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
24 | func (window Window) AudioContext() audio.AudioContext {
25 | constructor := window.Get("AudioContext")
26 | value := constructor.New().Value
27 | return audio.AudioContext{Value: value}
28 | }
29 |
30 | // SUBTYPE GETTERS
31 |
32 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/console
33 | // https://developer.mozilla.org/en-US/docs/Web/API/Console
34 | func (window Window) Console() Console {
35 | return Console{Value: window.Get("console")}
36 | }
37 |
38 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/document
39 | // https://developer.mozilla.org/en-US/docs/Web/API/Document
40 | func (window Window) Document() Document {
41 | return Document{Value: window.Get("document")}
42 | }
43 |
44 | // Event returns the current event.
45 | // The Event object passed directly to event handlers should be used instead whenever possible.
46 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/event
47 | func (window Window) Event() Event {
48 | return Event{Value: window.Get("event")}
49 | }
50 |
51 | // Navigator returns a reference to the Navigator object,
52 | // which has methods and properties about the application running the script.
53 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
54 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator
55 | func (window Window) Navigator() Navigator {
56 | return Navigator{Value: window.Get("navigator")}
57 | }
58 |
59 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
60 | // https://developer.mozilla.org/en-US/docs/Web/API/Screen
61 | func (window Window) Screen() Screen {
62 | return Screen{Value: window.Get("screen")}
63 | }
64 |
65 | // Create an object used to send HTTP requests (XMLHttpRequest),
66 | // open it (initialize) and set the response type (responseType) to binary.
67 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
68 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/XMLHttpRequest
69 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/open
70 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
71 | func (window Window) HTTPRequest(method, url string) HTTPRequest {
72 | req := HTTPRequest{
73 | Value: window.Get("XMLHttpRequest").New(),
74 | window: window,
75 | }
76 | req.Call("open", method, url, true)
77 | req.Set("responseType", "arraybuffer")
78 | return req
79 | }
80 |
81 | // OTHER GETTERS
82 |
83 | // Returns the height of the content area of the browser window including, if rendered, the horizontal scrollbar.
84 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerHeight
85 | func (window Window) InnerHeight() int {
86 | return window.Get("innerHeight").Int()
87 | }
88 |
89 | // Returns the width of the content area of the browser window including, if rendered, the vertical scrollbar.
90 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth
91 | func (window Window) InnerWidth() int {
92 | return window.Get("innerWidth").Int()
93 | }
94 |
95 | // Returns the height of the outside of the browser window.
96 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/outerHeight
97 | func (window Window) OuterHeight() int {
98 | return window.Get("outerHeight").Int()
99 | }
100 |
101 | // Returns the width of the outside of the browser window.
102 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/outerWidth
103 | func (window Window) OuterWidth() int {
104 | return window.Get("outerWidth").Int()
105 | }
106 |
107 | // Returns the horizontal distance from the left border of the user's browser viewport to the left side of the screen.
108 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/screenX
109 | func (window Window) ScreenX() int {
110 | return window.Get("screenX").Int()
111 | }
112 |
113 | // Returns the vertical distance from the top border of the user's browser viewport to the top side of the screen.
114 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/screenY
115 | func (window Window) ScreenY() int {
116 | return window.Get("screenY").Int()
117 | }
118 |
119 | // Returns the number of pixels that the document has already been scrolled horizontally.
120 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX
121 | func (window Window) ScrollX() int {
122 | return window.Get("scrollX").Int()
123 | }
124 |
125 | // Returns the number of pixels that the document has already been scrolled vertically.
126 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY
127 | func (window Window) ScrollY() int {
128 | return window.Get("scrollY").Int()
129 | }
130 |
131 | // SETTERS
132 |
133 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX
134 | func (window Window) SetScrollX(pixels int) {
135 | window.Set("scrollX", pixels)
136 | }
137 |
138 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY
139 | func (window Window) SetScrollY(pixels int) {
140 | window.Set("scrollY", pixels)
141 | }
142 |
143 | // METHODS
144 |
145 | // https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
146 | func (window Window) RequestAnimationFrame(handler func(), recursive bool) {
147 | wrapped := func(this js.Value, args []js.Value) any {
148 | handler()
149 | if recursive {
150 | window.RequestAnimationFrame(handler, true)
151 | }
152 | return nil
153 | }
154 | window.Call("requestAnimationFrame", js.FuncOf(wrapped))
155 | }
156 |
--------------------------------------------------------------------------------
/web/window_test.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "syscall/js"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestGetWindow(t *testing.T) {
11 | w := GetWindow()
12 | assert.Equal(t, w.Type(), js.TypeObject, "window is undefined")
13 | assert.Equal(t, w.Call("toString").String(), "[object Window]", "bad type")
14 | assert.Equal(t, w.Get("asdqwqwafd").Type(), js.TypeUndefined, "your types lie")
15 | }
16 |
17 | func TestWindowConsole(t *testing.T) {
18 | c := GetWindow().Console()
19 | assert.Equal(t, c.Type(), js.TypeObject, "console is undefined")
20 | }
21 |
22 | func TestWindowDocument(t *testing.T) {
23 | d := GetWindow().Document()
24 | assert.Equal(t, d.Type(), js.TypeObject, "document is undefined")
25 | assert.Equal(t, d.Call("toString").String(), "[object HTMLDocument]", "bad type")
26 | }
27 |
--------------------------------------------------------------------------------